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:
parent
a6d4494c0e
commit
9775db7af8
12 changed files with 190 additions and 27 deletions
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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:
|
||||||
*
|
*
|
||||||
|
|
36
packages/schemas/src/seeds/cloud-api.ts
Normal file
36
packages/schemas/src/seeds/cloud-api.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue