0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

refactor: add alteration scripts

This commit is contained in:
Gao Sun 2023-02-12 18:43:02 +08:00
parent c225719d70
commit 7a7d7f9f41
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
9 changed files with 249 additions and 33 deletions

View file

@ -1,7 +1,7 @@
import { generateKeyPair } from 'crypto';
import { promisify } from 'util';
import { nanoid } from 'nanoid';
import { generateStandardId } from '@logto/core-kit';
export const generateOidcPrivateKey = async (type: 'rsa' | 'ec' = 'ec') => {
if (type === 'rsa') {
@ -41,4 +41,4 @@ export const generateOidcPrivateKey = async (type: 'rsa' | 'ec' = 'ec') => {
throw new Error(`Unsupported private key ${String(type)}`);
};
export const generateOidcCookieKey = () => nanoid();
export const generateOidcCookieKey = () => generateStandardId();

View file

@ -21,8 +21,6 @@ export default function initMeApis(tenant: TenantContext): Koa {
const { findUserById, updateUserById } = tenant.queries.users;
const meRouter = new Router<unknown, WithAuthContext>();
console.log('????', getManagementApiResourceIndicator(adminTenantId, 'me'));
meRouter.use(
koaAuth(tenant.envSet, getManagementApiResourceIndicator(adminTenantId, 'me')),
async (ctx, next) => {

View file

@ -5,25 +5,6 @@ import { raw } from 'slonik-sql-tag-raw';
import type { AlterationScript } from '../lib/types/alteration.js';
const tables: string[] = [
'applications_roles',
'applications',
'connectors',
'custom_phrases',
'logs',
'logto_configs',
'oidc_model_instances',
'passcodes',
'resources',
'roles_scopes',
'roles',
'scopes',
'sign_in_experiences',
'users_roles',
'users',
'hooks',
];
const adminTenantId = 'admin';
const getId = (value: string) => sql.identifier([value]);

View file

@ -0,0 +1,32 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
drop index logs__user_id;
drop index logs__application_id;
create index logs__user_id
on logs (tenant_id, (payload->>'userId'));
create index logs__application_id
on logs (tenant_id, (payload->>'applicationId'));
`);
},
down: async (pool) => {
await pool.query(sql`
drop index logs__user_id;
drop index logs__application_id;
create index logs__user_id
on logs (tenant_id, (payload->>'user_id') nulls last);
create index logs__application_id
on logs (tenant_id, (payload->>'application_id') nulls last);
`);
},
};
export default alteration;

View file

@ -0,0 +1,208 @@
import { generateKeyPair } from 'crypto';
import { promisify } from 'util';
import { generateStandardId } from '@logto/core-kit';
import inquirer from 'inquirer';
import type { CommonQueryMethods, SerializableValue } from 'slonik';
import { sql } from '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) => {
// 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 user_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 has 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`,`
)};
`);
// Init admin OIDC configs
await updateConfigByKey(pool, adminTenantId, 'oidc.privateKeys', [
await generateOidcPrivateKey(),
]);
await updateConfigByKey(pool, adminTenantId, 'oidc.cookieKeys', [generateOidcCookieKey()]);
},
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;

View file

@ -43,6 +43,7 @@
"@silverhand/eslint-config": "1.3.0",
"@silverhand/essentials": "2.1.0",
"@silverhand/ts-config": "1.2.1",
"@types/inquirer": "^9.0.0",
"@types/jest": "^29.1.2",
"@types/node": "^18.11.18",
"@types/pluralize": "^0.0.29",

View file

@ -15,7 +15,7 @@ create index logs__key
on logs (tenant_id, key);
create index logs__user_id
on logs (tenant_id, (payload->>'user_id') nulls last);
on logs (tenant_id, (payload->>'userId'));
create index logs__application_id
on logs (tenant_id, (payload->>'application_id') nulls last);
on logs (tenant_id, (payload->>'applicationId'));

View file

@ -7,7 +7,7 @@ create table passcodes (
email varchar(128),
type varchar(32) not null,
code varchar(6) not null,
consumed boolean not null default FALSE,
consumed boolean not null default false,
try_count int2 not null default 0,
created_at timestamptz not null default(now()),
primary key (id)

10
pnpm-lock.yaml generated
View file

@ -586,6 +586,7 @@ importers:
'@silverhand/eslint-config': 1.3.0
'@silverhand/essentials': 2.1.0
'@silverhand/ts-config': 1.2.1
'@types/inquirer': ^9.0.0
'@types/jest': ^29.1.2
'@types/node': ^18.11.18
'@types/pluralize': ^0.0.29
@ -613,6 +614,7 @@ importers:
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
'@silverhand/essentials': 2.1.0
'@silverhand/ts-config': 1.2.1_typescript@4.9.4
'@types/inquirer': 9.0.3
'@types/jest': 29.1.2
'@types/node': 18.11.18
'@types/pluralize': 0.0.29
@ -4002,7 +4004,7 @@ packages:
resolution: {integrity: sha512-CzNkWqQftcmk2jaCWdBTf9Sm7xSw4rkI1zpU/Udw3HX5//adEZUIm9STtoRP1qgWj0CWQtJ9UTvqmO2NNjhMJw==}
dependencies:
'@types/through': 0.0.30
rxjs: 7.5.5
rxjs: 7.8.0
dev: true
/@types/is-ci/3.0.0:
@ -12927,12 +12929,6 @@ packages:
queue-microtask: 1.2.3
dev: true
/rxjs/7.5.5:
resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==}
dependencies:
tslib: 2.4.1
dev: true
/rxjs/7.8.0:
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
dependencies: