mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
208 lines
6.3 KiB
TypeScript
208 lines
6.3 KiB
TypeScript
import { generateKeyPair } from 'node:crypto';
|
|
import { promisify } from 'node:util';
|
|
|
|
import { generateStandardId } from '@logto/shared/universal';
|
|
import inquirer from 'inquirer';
|
|
import type { CommonQueryMethods, SerializableValue } from '@silverhand/slonik';
|
|
import { sql } from '@silverhand/slonik';
|
|
|
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
|
|
|
// Copied from CLI with default execution path
|
|
const generateOidcPrivateKey = async () => {
|
|
const { privateKey } = await promisify(generateKeyPair)('ec', {
|
|
// https://security.stackexchange.com/questions/78621/which-elliptic-curve-should-i-use
|
|
namedCurve: 'secp384r1',
|
|
publicKeyEncoding: {
|
|
type: 'spki',
|
|
format: 'pem',
|
|
},
|
|
privateKeyEncoding: {
|
|
type: 'pkcs8',
|
|
format: 'pem',
|
|
},
|
|
});
|
|
|
|
return privateKey;
|
|
};
|
|
|
|
const generateOidcCookieKey = () => generateStandardId();
|
|
|
|
// Edited from CLI
|
|
const updateConfigByKey = async <T>(
|
|
pool: CommonQueryMethods,
|
|
tenantId: string,
|
|
key: string,
|
|
value: SerializableValue
|
|
) =>
|
|
pool.query(
|
|
sql`
|
|
insert into logto_configs (tenant_id, key, value)
|
|
values (${tenantId}, ${key}, ${sql.jsonb(value)})
|
|
`
|
|
);
|
|
|
|
const adminTenantId = 'admin';
|
|
const defaultTenantId = 'default';
|
|
|
|
const alteration: AlterationScript = {
|
|
up: async (pool) => {
|
|
// Init admin OIDC configs
|
|
await updateConfigByKey(pool, adminTenantId, 'oidc.privateKeys', [
|
|
await generateOidcPrivateKey(),
|
|
]);
|
|
await updateConfigByKey(pool, adminTenantId, 'oidc.cookieKeys', [generateOidcCookieKey()]);
|
|
|
|
// Skipped tables:
|
|
// applications_roles, applications, connectors, custom_phrases, logto_configs,
|
|
// passcodes, resources, roles_scopes, roles, scopes, sign_in_experiences,
|
|
// systems, users_roles, hooks, tenants
|
|
//
|
|
// Migrate: logs, oidc_model_instances, users
|
|
|
|
// Find admin users
|
|
const { rows } = await pool.query<{ userId: string; count: number }>(sql`
|
|
select
|
|
users.id as "userId",
|
|
(select count(*) from users_roles where users.id = user_id)
|
|
from users
|
|
inner join users_roles on users.id = users_roles.user_id
|
|
inner join roles on roles.id = users_roles.role_id
|
|
where roles.name = 'admin';
|
|
`);
|
|
|
|
const invalidUsers = rows.filter(({ count }) => count > 1);
|
|
|
|
if (invalidUsers.length > 0) {
|
|
throw new Error(
|
|
'Some of your current admin users have extra roles. Either remove their `admin` role to become a normal user, or remove all other roles to migrate them to the new Admin Tenant.\n\n' +
|
|
'Invalid user IDs: ' +
|
|
invalidUsers.map(({ userId }) => userId).join(', ')
|
|
);
|
|
}
|
|
|
|
const userIds = rows.map(({ userId }) => userId);
|
|
|
|
if (userIds.length === 0) {
|
|
console.log('No admin user found, skip alteration');
|
|
|
|
return;
|
|
}
|
|
|
|
const inUserIds = sql`in (${sql.join(userIds, sql`, `)})`;
|
|
|
|
// Remove the admin role from users_roles
|
|
await pool.query(sql`
|
|
delete from users_roles
|
|
where user_id ${inUserIds};
|
|
`);
|
|
|
|
// Update data
|
|
console.warn(
|
|
'Some of the logs will stay in the default tenant since the related interaction has been removed.'
|
|
);
|
|
|
|
await pool.query(sql`
|
|
update users
|
|
set tenant_id = ${adminTenantId}
|
|
where id ${inUserIds};
|
|
`);
|
|
await pool.query(sql`
|
|
update logs
|
|
set tenant_id = ${adminTenantId}
|
|
where payload->>'userId' ${inUserIds};
|
|
`);
|
|
await pool.query(sql`
|
|
update oidc_model_instances
|
|
set tenant_id = ${adminTenantId}
|
|
where payload->>'accountId' ${inUserIds};
|
|
`);
|
|
|
|
// Assign roles
|
|
const { rows: roles } = await pool.query<{ id: string }>(sql`
|
|
select id from roles
|
|
where tenant_id = ${adminTenantId}
|
|
and (name = ${'default:admin'} or name = ${'user'})
|
|
`);
|
|
|
|
if (roles.length !== 2) {
|
|
throw new Error('Admin tenant should have both `default:admin` and `user` role.');
|
|
}
|
|
|
|
await pool.query(sql`
|
|
insert into users_roles (tenant_id, id, user_id, role_id)
|
|
values ${sql.join(
|
|
userIds.flatMap((userId) =>
|
|
roles.map(
|
|
({ id }) => sql`(${adminTenantId}, ${generateStandardId()}, ${userId}, ${id})`
|
|
)
|
|
),
|
|
sql`,`
|
|
)};
|
|
`);
|
|
},
|
|
down: async (pool) => {
|
|
const { rows } = await pool.query<{ id: string }>(sql`select id from tenants;`);
|
|
const tenantIds = rows
|
|
.map(({ id }) => id)
|
|
.slice()
|
|
.sort((i, j) => i.localeCompare(j));
|
|
|
|
if (!(tenantIds.length === 2 && tenantIds[0] === 'admin' && tenantIds[1] === 'default')) {
|
|
throw new Error('The tenants table should only have exact `admin` and `default` tenant.');
|
|
}
|
|
|
|
const isCi = process.env.CI;
|
|
const { confirm } = await inquirer.prompt<{ confirm: boolean }>({
|
|
type: 'confirm',
|
|
name: 'confirm',
|
|
message: String(
|
|
'***CAUTION***\n' +
|
|
'The `down()` function will restore Admin Tenant users to the default tenant.\n' +
|
|
'Except `users`, and `logs`, ALL other data will be dropped.\n' +
|
|
'Are you sure to continue?'
|
|
),
|
|
default: false,
|
|
when: !isCi,
|
|
});
|
|
|
|
if (!isCi && !confirm) {
|
|
throw new Error('User cancelled alteration.');
|
|
}
|
|
|
|
const { rows: adminUsers } = await pool.query<{ id: string }>(sql`
|
|
select users.id from users
|
|
inner join users_roles on users.id = users_roles.user_id
|
|
inner join roles on roles.id = users_roles.role_id
|
|
where roles.name = 'default:admin'
|
|
and users.tenant_id = 'admin';
|
|
`);
|
|
const adminUserIds = adminUsers.map(({ id }) => id);
|
|
|
|
if (adminUserIds.length > 0) {
|
|
await pool.query(sql`
|
|
insert into users_roles (tenant_id, id, user_id, role_id)
|
|
values ${sql.join(
|
|
adminUserIds.map(
|
|
(id) => sql`(${defaultTenantId}, ${generateStandardId()}, ${id}, ${'admin-role'})`
|
|
),
|
|
sql`,`
|
|
)};
|
|
`);
|
|
|
|
console.log(`Converted admin role for user ID(s): ${adminUserIds.join(', ')}`);
|
|
}
|
|
|
|
await pool.query(sql`
|
|
update users set tenant_id = ${defaultTenantId};
|
|
`);
|
|
await pool.query(sql`
|
|
update logs set tenant_id = ${defaultTenantId};
|
|
`);
|
|
await pool.query(sql`
|
|
delete from tenants where id = ${adminTenantId};
|
|
`);
|
|
},
|
|
};
|
|
|
|
export default alteration;
|