2023-04-04 03:23:25 -05:00
import { generateKeyPair } from 'node:crypto' ;
import { promisify } from 'node:util' ;
2023-02-12 05:43:02 -05:00
2023-04-04 03:23:25 -05:00
import { generateStandardId } from '@logto/shared/universal' ;
2023-02-12 05:43:02 -05:00
import inquirer from 'inquirer' ;
2024-03-16 06:04:55 -05:00
import type { CommonQueryMethods , SerializableValue } from '@silverhand/slonik' ;
import { sql } from '@silverhand/slonik' ;
2023-02-12 05:43:02 -05:00
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 ) = > {
2023-02-15 10:30:27 -05:00
// Init admin OIDC configs
await updateConfigByKey ( pool , adminTenantId , 'oidc.privateKeys' , [
await generateOidcPrivateKey ( ) ,
] ) ;
await updateConfigByKey ( pool , adminTenantId , 'oidc.cookieKeys' , [ generateOidcCookieKey ( ) ] ) ;
2023-02-12 05:43:02 -05:00
// 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" ,
2023-02-15 19:34:16 -05:00
( select count ( * ) from users_roles where users . id = user_id )
2023-02-12 05:43:02 -05:00
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 (
2023-02-15 19:34:16 -05:00
'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' +
2023-02-12 05:43:02 -05:00
'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 ;