From 9775db7af8688a7463d61c9cb6dda300cb5e1cf5 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 2 Mar 2023 22:25:13 +0800 Subject: [PATCH] refactor: seed data for multi-tenancy --- .../cli/src/commands/database/seed/tables.ts | 4 + .../cli/src/commands/database/seed/tenant.ts | 29 ++++- packages/cloud/src/middleware/with-auth.ts | 3 +- packages/core/src/env-set/GlobalValues.ts | 5 + .../core/src/middleware/koa-auth/utils.ts | 12 +- packages/core/src/tenants/Tenant.ts | 8 +- packages/core/src/utils/tenant.ts | 4 +- .../next-1677765137-seed-for-admin-tenant.ts | 103 ++++++++++++++++++ packages/schemas/src/consts/system.ts | 7 -- packages/schemas/src/seeds/cloud-api.ts | 36 ++++++ packages/schemas/src/seeds/index.ts | 1 + packages/schemas/src/seeds/management-api.ts | 5 + 12 files changed, 190 insertions(+), 27 deletions(-) create mode 100644 packages/schemas/alterations/next-1677765137-seed-for-admin-tenant.ts create mode 100644 packages/schemas/src/seeds/cloud-api.ts diff --git a/packages/cli/src/commands/database/seed/tables.ts b/packages/cli/src/commands/database/seed/tables.ts index be2778e73..b5dc3c364 100644 --- a/packages/cli/src/commands/database/seed/tables.ts +++ b/packages/cli/src/commands/database/seed/tables.ts @@ -11,6 +11,7 @@ import { createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, + createCloudApi, } from '@logto/schemas'; import { Hooks, Tenants } from '@logto/schemas/models'; import type { DatabaseTransactionConnection } from 'slonik'; @@ -121,13 +122,16 @@ export const seedTables = async ( await createTenant(connection, adminTenantId); await seedOidcConfigs(connection, adminTenantId); await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId)); + await seedAdminData(connection, createAdminDataInAdminTenant(adminTenantId)); await seedAdminData(connection, createMeApiInAdminTenant()); + await seedAdminData(connection, createCloudApi()); await Promise.all([ connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')), connection.query( insertInto(createDefaultSignInExperience(defaultTenantId), 'sign_in_experiences') ), + connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')), connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')), connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')), updateDatabaseTimestamp(connection, latestTimestamp), diff --git a/packages/cli/src/commands/database/seed/tenant.ts b/packages/cli/src/commands/database/seed/tenant.ts index b5a325425..be7f40f33 100644 --- a/packages/cli/src/commands/database/seed/tenant.ts +++ b/packages/cli/src/commands/database/seed/tenant.ts @@ -1,6 +1,6 @@ import { generateStandardId } from '@logto/core-kit'; +import type { TenantModel, AdminData, UpdateAdminData } from '@logto/schemas'; import { CreateRolesScope } from '@logto/schemas'; -import type { TenantModel, AdminData } from '@logto/schemas'; import { createTenantMetadata } from '@logto/shared'; import { assert } from '@silverhand/essentials'; import type { CommonQueryMethods } from 'slonik'; @@ -23,7 +23,10 @@ export const createTenant = async (pool: CommonQueryMethods, tenantId: string) = `); }; -export const seedAdminData = async (pool: CommonQueryMethods, data: AdminData) => { +export const seedAdminData = async ( + pool: CommonQueryMethods, + data: AdminData | UpdateAdminData +) => { const { resource, scope, role } = data; assert( @@ -31,14 +34,32 @@ export const seedAdminData = async (pool: CommonQueryMethods, data: AdminData) = new Error('All data should have the same tenant ID') ); + const processRole = async () => { + if ('id' in role) { + await pool.query(insertInto(role, 'roles')); + + return role.id; + } + + // Query by role name for existing roles + const { id } = await pool.one<{ id: string }>(sql` + select id from roles + where name=${role.name} + and tenant_id=${String(role.tenantId)} + `); + + return id; + }; + await pool.query(insertInto(resource, 'resources')); await pool.query(insertInto(scope, 'scopes')); - await pool.query(insertInto(role, 'roles')); + + const roleId = await processRole(); await pool.query( insertInto( { id: generateStandardId(), - roleId: role.id, + roleId, scopeId: scope.id, tenantId: resource.tenantId, } satisfies CreateRolesScope, diff --git a/packages/cloud/src/middleware/with-auth.ts b/packages/cloud/src/middleware/with-auth.ts index ac2c96302..67b367e8e 100644 --- a/packages/cloud/src/middleware/with-auth.ts +++ b/packages/cloud/src/middleware/with-auth.ts @@ -76,7 +76,8 @@ export default function withAuth({ audience, }), (error) => { - throw error; + console.error(error); + throw new RequestError('JWT verification failed.', 401); } ); diff --git a/packages/core/src/env-set/GlobalValues.ts b/packages/core/src/env-set/GlobalValues.ts index 4d4d63c17..1a8d3ad9c 100644 --- a/packages/core/src/env-set/GlobalValues.ts +++ b/packages/core/src/env-set/GlobalValues.ts @@ -80,6 +80,11 @@ export default class GlobalValues { public readonly isPathBasedMultiTenancy = !this.isDomainBasedMultiTenancy && yes(getEnv('PATH_BASED_MULTI_TENANCY')); + /** Alias for `isDomainBasedMultiTenancy || isPathBasedMultiTenancy`. */ + public get isMultiTenancy(): boolean { + return this.isDomainBasedMultiTenancy || this.isPathBasedMultiTenancy; + } + // eslint-disable-next-line unicorn/consistent-function-scoping public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage); public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID'); diff --git a/packages/core/src/middleware/koa-auth/utils.ts b/packages/core/src/middleware/koa-auth/utils.ts index 5ab78b94b..a9b49ec27 100644 --- a/packages/core/src/middleware/koa-auth/utils.ts +++ b/packages/core/src/middleware/koa-auth/utils.ts @@ -27,13 +27,9 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{ keys: JWK[]; issuer: string[]; }> => { - const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy, adminUrlSet } = EnvSet.values; + const { isMultiTenancy, adminUrlSet } = EnvSet.values; - if ( - !isDomainBasedMultiTenancy && - !isPathBasedMultiTenancy && - adminUrlSet.deduplicated().length === 0 - ) { + if (!isMultiTenancy && adminUrlSet.deduplicated().length === 0) { return { keys: [], issuer: [] }; } @@ -52,9 +48,7 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{ keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))), issuer: [ appendPath( - isDomainBasedMultiTenancy || isPathBasedMultiTenancy - ? getTenantEndpoint(adminTenantId, EnvSet.values) - : adminUrlSet.endpoint, + isMultiTenancy ? getTenantEndpoint(adminTenantId, EnvSet.values) : adminUrlSet.endpoint, '/oidc' ).toString(), ], diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index ea7a44407..39b0c7f6d 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -87,7 +87,7 @@ export default class Tenant implements TenantContext { // Mount APIs app.use(mount('/api', initApis(tenantContext))); - const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy } = EnvSet.values; + const { isMultiTenancy } = EnvSet.values; // Mount admin tenant APIs and app if (id === adminTenantId) { @@ -95,8 +95,8 @@ export default class Tenant implements TenantContext { app.use(mount('/me', initMeApis(tenantContext))); // Mount Admin Console when needed - // Skip in domain-based multi-tenancy since Logto Cloud serves Admin Console in this case - if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) { + // Skip in multi-tenancy mode since Logto Cloud serves Admin Console in this case + if (!isMultiTenancy) { app.use(koaConsoleRedirectProxy(queries)); app.use( mount( @@ -111,7 +111,7 @@ export default class Tenant implements TenantContext { // while distinguishing "demo app from admin tenant" and "demo app from user tenant"; // on the cloud, we need to configure admin tenant sign-in experience, so a preview is needed for // testing without signing out of the admin console. - if (id !== adminTenantId || isDomainBasedMultiTenancy || isPathBasedMultiTenancy) { + if (id !== adminTenantId || isMultiTenancy) { // Mount demo app app.use( mount( diff --git a/packages/core/src/utils/tenant.ts b/packages/core/src/utils/tenant.ts index 34c7d4bf6..fccdf9dd5 100644 --- a/packages/core/src/utils/tenant.ts +++ b/packages/core/src/utils/tenant.ts @@ -43,7 +43,7 @@ const matchPathBasedTenantId = (urlSet: UrlSet, url: URL) => { export const getTenantId = (url: URL) => { const { - isDomainBasedMultiTenancy, + isMultiTenancy, isPathBasedMultiTenancy, isProduction, isIntegrationTest, @@ -62,7 +62,7 @@ export const getTenantId = (url: URL) => { return developmentTenantId; } - if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) { + if (!isMultiTenancy) { return defaultTenantId; } diff --git a/packages/schemas/alterations/next-1677765137-seed-for-admin-tenant.ts b/packages/schemas/alterations/next-1677765137-seed-for-admin-tenant.ts new file mode 100644 index 000000000..d557a64ff --- /dev/null +++ b/packages/schemas/alterations/next-1677765137-seed-for-admin-tenant.ts @@ -0,0 +1,103 @@ +import { generateStandardId } from '@logto/core-kit'; +import type { CommonQueryMethods } from 'slonik'; +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const adminTenantId = 'admin'; + +const addApiData = async (pool: CommonQueryMethods) => { + const adminApi = { + resourceId: generateStandardId(), + scopeId: generateStandardId(), + }; + const cloudApi = { + resourceId: generateStandardId(), + scopeId: generateStandardId(), + }; + + await pool.query(sql` + insert into resources (tenant_id, id, indicator, name) + values ( + ${adminTenantId}, + ${adminApi.resourceId}, + 'https://admin.logto.app/api', + 'Logto Management API for tenant admin' + ), ( + ${adminTenantId}, + ${cloudApi.resourceId}, + 'https://cloud.logto.io/api', + 'Logto Management API for tenant admin' + ); + `); + await pool.query(sql` + insert into scopes (tenant_id, id, name, description, resource_id) + values ( + ${adminTenantId}, + ${adminApi.scopeId}, + 'all', + 'Default scope for Management API, allows all permissions.', + ${adminApi.scopeId} + ), ( + ${adminTenantId}, + ${cloudApi.scopeId}, + 'create:tenant', + 'Allow creating new tenants.', + ${cloudApi.scopeId} + ); + `); + + const { id: roleId } = await pool.one<{ id: string }>(sql` + select id from roles + where tenant_id = ${adminTenantId} + and name = 'user' + `); + + await pool.query(sql` + insert into roles_scopes (tenant_id, id, role_id, scope_id) + values ( + ${adminTenantId}, + ${generateStandardId()}, + ${roleId}, + ${adminApi.scopeId} + ), ( + ${adminTenantId}, + ${generateStandardId()}, + ${roleId}, + ${cloudApi.scopeId} + ); + `); +}; + +const alteration: AlterationScript = { + up: async (pool) => { + await addApiData(pool); + await pool.query(sql` + insert into logto_configs (tenant_id, key, value) + values ( + ${adminTenantId}, + 'adminConsole', + ${sql.jsonb({ + language: 'en', + appearanceMode: 'system', + livePreviewChecked: false, + applicationCreated: false, + signInExperienceCustomized: false, + passwordlessConfigured: false, + selfHostingChecked: false, + communityChecked: false, + m2mApplicationCreated: false, + })} + ); + `); + }, + down: async (pool) => { + await pool.query(sql` + delete from applications + where tenant_id = 'admin' + and id = 'admin-console'; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/consts/system.ts b/packages/schemas/src/consts/system.ts index 1584eadb7..153dddf84 100644 --- a/packages/schemas/src/consts/system.ts +++ b/packages/schemas/src/consts/system.ts @@ -1,10 +1,3 @@ -/** The API Resource Indicator for Logto Cloud. It's only useful when domain-based multi-tenancy is enabled. */ -export const cloudApiIndicator = 'https://cloud.logto.io/api'; - -export enum CloudScope { - CreateTenant = 'create:tenant', -} - /** * In OSS: * diff --git a/packages/schemas/src/seeds/cloud-api.ts b/packages/schemas/src/seeds/cloud-api.ts new file mode 100644 index 000000000..829fa21b8 --- /dev/null +++ b/packages/schemas/src/seeds/cloud-api.ts @@ -0,0 +1,36 @@ +import { generateStandardId } from '@logto/core-kit'; + +import { UserRole } from '../types/index.js'; +import type { UpdateAdminData } from './management-api.js'; +import { adminTenantId } from './tenant.js'; + +/** The API Resource Indicator for Logto Cloud. It's only useful when domain-based multi-tenancy is enabled. */ +export const cloudApiIndicator = 'https://cloud.logto.io/api'; + +export enum CloudScope { + CreateTenant = 'create:tenant', +} + +export const createCloudApi = (): Readonly => { + const resourceId = generateStandardId(); + + return Object.freeze({ + resource: { + tenantId: adminTenantId, + id: resourceId, + indicator: cloudApiIndicator, + name: `Logto Cloud API`, + }, + scope: { + tenantId: adminTenantId, + id: generateStandardId(), + name: CloudScope.CreateTenant, + description: 'Allow creating new tenants.', + resourceId, + }, + role: { + tenantId: adminTenantId, + name: UserRole.User, + }, + }); +}; diff --git a/packages/schemas/src/seeds/index.ts b/packages/schemas/src/seeds/index.ts index 4e88b5a3f..0e5d396c8 100644 --- a/packages/schemas/src/seeds/index.ts +++ b/packages/schemas/src/seeds/index.ts @@ -1,4 +1,5 @@ export * from './application.js'; +export * from './cloud-api.js'; export * from './management-api.js'; export * from './logto-config.js'; export * from './sign-in-experience.js'; diff --git a/packages/schemas/src/seeds/management-api.ts b/packages/schemas/src/seeds/management-api.ts index 13e18f08e..11d511cd9 100644 --- a/packages/schemas/src/seeds/management-api.ts +++ b/packages/schemas/src/seeds/management-api.ts @@ -10,6 +10,11 @@ export type AdminData = { role: CreateRole; }; +export type UpdateAdminData = Omit & { + /** Attach to an existing role instead of creating one. */ + role: Pick; +}; + // Consider remove the dependency of IDs const defaultResourceId = 'management-api'; const defaultScopeAllId = 'management-api-all';