0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -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,
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),

View file

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

View file

@ -76,7 +76,8 @@ export default function withAuth<InputContext extends RequestContext>({
audience,
}),
(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 =
!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');

View file

@ -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(),
],

View file

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

View file

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

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:
*

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 './cloud-api.js';
export * from './management-api.js';
export * from './logto-config.js';
export * from './sign-in-experience.js';

View file

@ -10,6 +10,11 @@ export type AdminData = {
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
const defaultResourceId = 'management-api';
const defaultScopeAllId = 'management-api-all';