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:
parent
c225719d70
commit
7a7d7f9f41
9 changed files with 249 additions and 33 deletions
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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",
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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
10
pnpm-lock.yaml
generated
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue