0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor: seed data for multi-tenancy

This commit is contained in:
Gao Sun 2023-03-02 22:25:13 +08:00
parent a6d4494c0e
commit 9775db7af8
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
12 changed files with 190 additions and 27 deletions

View file

@ -11,6 +11,7 @@ import {
createDefaultSignInExperience, createDefaultSignInExperience,
createAdminTenantSignInExperience, createAdminTenantSignInExperience,
createDefaultAdminConsoleApplication, createDefaultAdminConsoleApplication,
createCloudApi,
} from '@logto/schemas'; } from '@logto/schemas';
import { Hooks, Tenants } from '@logto/schemas/models'; import { Hooks, Tenants } from '@logto/schemas/models';
import type { DatabaseTransactionConnection } from 'slonik'; import type { DatabaseTransactionConnection } from 'slonik';
@ -121,13 +122,16 @@ export const seedTables = async (
await createTenant(connection, adminTenantId); await createTenant(connection, adminTenantId);
await seedOidcConfigs(connection, adminTenantId); await seedOidcConfigs(connection, adminTenantId);
await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId)); await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId));
await seedAdminData(connection, createAdminDataInAdminTenant(adminTenantId));
await seedAdminData(connection, createMeApiInAdminTenant()); await seedAdminData(connection, createMeApiInAdminTenant());
await seedAdminData(connection, createCloudApi());
await Promise.all([ await Promise.all([
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')), connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
connection.query( connection.query(
insertInto(createDefaultSignInExperience(defaultTenantId), 'sign_in_experiences') insertInto(createDefaultSignInExperience(defaultTenantId), 'sign_in_experiences')
), ),
connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')),
connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')), connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')), connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
updateDatabaseTimestamp(connection, latestTimestamp), updateDatabaseTimestamp(connection, latestTimestamp),

View file

@ -1,6 +1,6 @@
import { generateStandardId } from '@logto/core-kit'; import { generateStandardId } from '@logto/core-kit';
import type { TenantModel, AdminData, UpdateAdminData } from '@logto/schemas';
import { CreateRolesScope } from '@logto/schemas'; import { CreateRolesScope } from '@logto/schemas';
import type { TenantModel, AdminData } from '@logto/schemas';
import { createTenantMetadata } from '@logto/shared'; import { createTenantMetadata } from '@logto/shared';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import type { CommonQueryMethods } from 'slonik'; 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; const { resource, scope, role } = data;
assert( assert(
@ -31,14 +34,32 @@ export const seedAdminData = async (pool: CommonQueryMethods, data: AdminData) =
new Error('All data should have the same tenant ID') 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(resource, 'resources'));
await pool.query(insertInto(scope, 'scopes')); await pool.query(insertInto(scope, 'scopes'));
await pool.query(insertInto(role, 'roles'));
const roleId = await processRole();
await pool.query( await pool.query(
insertInto( insertInto(
{ {
id: generateStandardId(), id: generateStandardId(),
roleId: role.id, roleId,
scopeId: scope.id, scopeId: scope.id,
tenantId: resource.tenantId, tenantId: resource.tenantId,
} satisfies CreateRolesScope, } satisfies CreateRolesScope,

View file

@ -76,7 +76,8 @@ export default function withAuth<InputContext extends RequestContext>({
audience, audience,
}), }),
(error) => { (error) => {
throw error; console.error(error);
throw new RequestError('JWT verification failed.', 401);
} }
); );

View file

@ -80,6 +80,11 @@ export default class GlobalValues {
public readonly isPathBasedMultiTenancy = public readonly isPathBasedMultiTenancy =
!this.isDomainBasedMultiTenancy && yes(getEnv('PATH_BASED_MULTI_TENANCY')); !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 // eslint-disable-next-line unicorn/consistent-function-scoping
public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage); public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID'); public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID');

View file

@ -27,13 +27,9 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
keys: JWK[]; keys: JWK[];
issuer: string[]; issuer: string[];
}> => { }> => {
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy, adminUrlSet } = EnvSet.values; const { isMultiTenancy, adminUrlSet } = EnvSet.values;
if ( if (!isMultiTenancy && adminUrlSet.deduplicated().length === 0) {
!isDomainBasedMultiTenancy &&
!isPathBasedMultiTenancy &&
adminUrlSet.deduplicated().length === 0
) {
return { keys: [], issuer: [] }; return { keys: [], issuer: [] };
} }
@ -52,9 +48,7 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))), keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))),
issuer: [ issuer: [
appendPath( appendPath(
isDomainBasedMultiTenancy || isPathBasedMultiTenancy isMultiTenancy ? getTenantEndpoint(adminTenantId, EnvSet.values) : adminUrlSet.endpoint,
? getTenantEndpoint(adminTenantId, EnvSet.values)
: adminUrlSet.endpoint,
'/oidc' '/oidc'
).toString(), ).toString(),
], ],

View file

@ -87,7 +87,7 @@ export default class Tenant implements TenantContext {
// Mount APIs // Mount APIs
app.use(mount('/api', initApis(tenantContext))); app.use(mount('/api', initApis(tenantContext)));
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy } = EnvSet.values; const { isMultiTenancy } = EnvSet.values;
// Mount admin tenant APIs and app // Mount admin tenant APIs and app
if (id === adminTenantId) { if (id === adminTenantId) {
@ -95,8 +95,8 @@ export default class Tenant implements TenantContext {
app.use(mount('/me', initMeApis(tenantContext))); app.use(mount('/me', initMeApis(tenantContext)));
// Mount Admin Console when needed // Mount Admin Console when needed
// Skip in domain-based multi-tenancy since Logto Cloud serves Admin Console in this case // Skip in multi-tenancy mode since Logto Cloud serves Admin Console in this case
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) { if (!isMultiTenancy) {
app.use(koaConsoleRedirectProxy(queries)); app.use(koaConsoleRedirectProxy(queries));
app.use( app.use(
mount( mount(
@ -111,7 +111,7 @@ export default class Tenant implements TenantContext {
// while distinguishing "demo app from admin tenant" and "demo app from user tenant"; // 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 // 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. // testing without signing out of the admin console.
if (id !== adminTenantId || isDomainBasedMultiTenancy || isPathBasedMultiTenancy) { if (id !== adminTenantId || isMultiTenancy) {
// Mount demo app // Mount demo app
app.use( app.use(
mount( mount(

View file

@ -43,7 +43,7 @@ const matchPathBasedTenantId = (urlSet: UrlSet, url: URL) => {
export const getTenantId = (url: URL) => { export const getTenantId = (url: URL) => {
const { const {
isDomainBasedMultiTenancy, isMultiTenancy,
isPathBasedMultiTenancy, isPathBasedMultiTenancy,
isProduction, isProduction,
isIntegrationTest, isIntegrationTest,
@ -62,7 +62,7 @@ export const getTenantId = (url: URL) => {
return developmentTenantId; return developmentTenantId;
} }
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) { if (!isMultiTenancy) {
return defaultTenantId; return defaultTenantId;
} }

View file

@ -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;

View file

@ -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: * In OSS:
* *

View file

@ -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<UpdateAdminData> => {
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,
},
});
};

View file

@ -1,4 +1,5 @@
export * from './application.js'; export * from './application.js';
export * from './cloud-api.js';
export * from './management-api.js'; export * from './management-api.js';
export * from './logto-config.js'; export * from './logto-config.js';
export * from './sign-in-experience.js'; export * from './sign-in-experience.js';

View file

@ -10,6 +10,11 @@ export type AdminData = {
role: CreateRole; role: CreateRole;
}; };
export type UpdateAdminData = Omit<AdminData, 'role'> & {
/** Attach to an existing role instead of creating one. */
role: Pick<CreateRole, 'tenantId' | 'name'>;
};
// Consider remove the dependency of IDs // Consider remove the dependency of IDs
const defaultResourceId = 'management-api'; const defaultResourceId = 'management-api';
const defaultScopeAllId = 'management-api-all'; const defaultScopeAllId = 'management-api-all';