mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(cli,cloud,schemas): seed m2m app for each tenant (#3364)
This commit is contained in:
parent
922e3a3a38
commit
1f618a3a10
11 changed files with 322 additions and 2 deletions
|
@ -176,6 +176,7 @@ const queryDatabaseData = async (database) => {
|
|||
'id',
|
||||
'resource_id',
|
||||
'role_id',
|
||||
'application_id',
|
||||
'scope_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
|
|
|
@ -12,6 +12,10 @@ import {
|
|||
createAdminTenantSignInExperience,
|
||||
createDefaultAdminConsoleApplication,
|
||||
createCloudApi,
|
||||
createTenantApplicationRole,
|
||||
createTenantMachineToMachineApplication,
|
||||
createAdminTenantApplicationRole,
|
||||
CloudScope,
|
||||
} from '@logto/schemas';
|
||||
import { Hooks, Tenants } from '@logto/schemas/models';
|
||||
import type { DatabaseTransactionConnection } from 'slonik';
|
||||
|
@ -128,7 +132,27 @@ export const seedTables = async (
|
|||
await seedAdminData(connection, createMeApiInAdminTenant());
|
||||
|
||||
const [cloudData, ...cloudAdditionalScopes] = createCloudApi();
|
||||
const applicationRole = createTenantApplicationRole();
|
||||
await seedAdminData(connection, cloudData, ...cloudAdditionalScopes);
|
||||
await connection.query(insertInto(applicationRole, 'roles'));
|
||||
await assignScopesToRole(
|
||||
connection,
|
||||
adminTenantId,
|
||||
applicationRole.id,
|
||||
...cloudAdditionalScopes
|
||||
.filter(({ name }) => name === CloudScope.SendSms || name === CloudScope.SendEmail)
|
||||
.map(({ id }) => id)
|
||||
);
|
||||
|
||||
// Add M2M app for default tenant
|
||||
const defaultTenantApplication = createTenantMachineToMachineApplication(defaultTenantId);
|
||||
await connection.query(insertInto(defaultTenantApplication, 'applications'));
|
||||
await connection.query(
|
||||
insertInto(
|
||||
createAdminTenantApplicationRole(defaultTenantApplication.id, applicationRole.id),
|
||||
'applications_roles'
|
||||
)
|
||||
);
|
||||
|
||||
// Assign all cloud API scopes to role `admin:admin`
|
||||
await assignScopesToRole(
|
||||
|
|
|
@ -5,6 +5,9 @@ import {
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { LogtoOidcConfigType, TenantInfo, TenantModel } from '@logto/schemas';
|
||||
import {
|
||||
createAdminTenantApplicationRole,
|
||||
AdminTenantRole,
|
||||
createTenantMachineToMachineApplication,
|
||||
LogtoOidcConfigKey,
|
||||
LogtoConfigs,
|
||||
SignInExperiences,
|
||||
|
@ -18,7 +21,9 @@ import { createTenantMetadata } from '@logto/shared';
|
|||
import type { ZodType } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createApplicationsQueries } from '#src/queries/application.js';
|
||||
import type { Queries } from '#src/queries/index.js';
|
||||
import { createRolesQuery } from '#src/queries/roles.js';
|
||||
import { createTenantsQueries } from '#src/queries/tenants.js';
|
||||
import { createUsersQueries } from '#src/queries/users.js';
|
||||
import { getDatabaseName } from '#src/queries/utils.js';
|
||||
|
@ -61,6 +66,8 @@ export class TenantsLibrary {
|
|||
const transaction = await this.queries.client.transaction();
|
||||
const tenants = createTenantsQueries(transaction);
|
||||
const users = createUsersQueries(transaction);
|
||||
const applications = createApplicationsQueries(transaction);
|
||||
const roles = createRolesQuery(transaction);
|
||||
|
||||
/* --- Start --- */
|
||||
await transaction.start();
|
||||
|
@ -79,6 +86,16 @@ export class TenantsLibrary {
|
|||
userId: forUserId,
|
||||
roleId: adminDataInAdminTenant.role.id,
|
||||
});
|
||||
// Create M2M App
|
||||
const m2mRoleId = await roles.findRoleIdByName(
|
||||
AdminTenantRole.TenantApplication,
|
||||
adminTenantId
|
||||
);
|
||||
const m2mApplication = createTenantMachineToMachineApplication(tenantId);
|
||||
await applications.insertApplication(m2mApplication);
|
||||
await applications.assignRoleToApplication(
|
||||
createAdminTenantApplicationRole(m2mApplication.id, m2mRoleId)
|
||||
);
|
||||
|
||||
// Create initial configs
|
||||
await Promise.all([
|
||||
|
|
17
packages/cloud/src/queries/application.ts
Normal file
17
packages/cloud/src/queries/application.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { CreateApplication, CreateApplicationsRole } from '@logto/schemas';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
||||
export type ApplicationsQuery = ReturnType<typeof createApplicationsQueries>;
|
||||
|
||||
export const createApplicationsQueries = (client: Queryable<PostgreSql>) => {
|
||||
const insertApplication = async (data: CreateApplication) =>
|
||||
client.query(insertInto(data, 'applications'));
|
||||
|
||||
const assignRoleToApplication = async (data: CreateApplicationsRole) =>
|
||||
client.query(insertInto(data, 'applications_roles'));
|
||||
|
||||
return { insertApplication, assignRoleToApplication };
|
||||
};
|
|
@ -3,6 +3,7 @@ import { createQueryClient } from '@withtyped/postgres';
|
|||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { parseDsn } from '#src/utils/postgres.js';
|
||||
|
||||
import { createApplicationsQueries } from './application.js';
|
||||
import { createTenantsQueries } from './tenants.js';
|
||||
import { createUsersQueries } from './users.js';
|
||||
|
||||
|
@ -12,4 +13,5 @@ export class Queries {
|
|||
public readonly client = createQueryClient(parseDsn(EnvSet.global.dbUrl));
|
||||
public readonly tenants = createTenantsQueries(this.client);
|
||||
public readonly users = createUsersQueries(this.client);
|
||||
public readonly applications = createApplicationsQueries(this.client);
|
||||
}
|
||||
|
|
23
packages/cloud/src/queries/roles.ts
Normal file
23
packages/cloud/src/queries/roles.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import { sql } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
|
||||
export type RolesQuery = ReturnType<typeof createRolesQuery>;
|
||||
|
||||
export const createRolesQuery = (client: Queryable<PostgreSql>) => {
|
||||
const findRoleIdByName = async (roleName: string, tenantId: string) => {
|
||||
const { rows } = await client.query<{ id: string }>(sql`
|
||||
select id from roles
|
||||
where name=${roleName}
|
||||
and tenant_id=${tenantId}
|
||||
`);
|
||||
|
||||
if (!rows[0]) {
|
||||
throw new Error(`Role ${roleName} not found.`);
|
||||
}
|
||||
|
||||
return rows[0].id;
|
||||
};
|
||||
|
||||
return { findRoleIdByName };
|
||||
};
|
|
@ -0,0 +1,179 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const adminTenantId = 'admin';
|
||||
const adminRoleName = 'admin:admin';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
const scopeIds = {
|
||||
sms: generateStandardId(),
|
||||
email: generateStandardId(),
|
||||
};
|
||||
const { id: resourceId } = await pool.one<{ id: string }>(sql`
|
||||
select id from resources
|
||||
where tenant_id = ${adminTenantId}
|
||||
and indicator = 'https://cloud.logto.io/api'
|
||||
`);
|
||||
|
||||
// Insert scopes
|
||||
await pool.query(sql`
|
||||
insert into scopes (tenant_id, id, name, description, resource_id)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${scopeIds.sms},
|
||||
'send:sms',
|
||||
'Allow sending SMS. This scope is only available to M2M application.',
|
||||
${resourceId}
|
||||
), (
|
||||
${adminTenantId},
|
||||
${scopeIds.email},
|
||||
'send:email',
|
||||
'Allow sending emails. This scope is only available to M2M application.',
|
||||
${resourceId}
|
||||
);
|
||||
`);
|
||||
|
||||
// Insert role
|
||||
const roleId = generateStandardId();
|
||||
await pool.query(sql`
|
||||
insert into roles (tenant_id, id, name, description)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${roleId},
|
||||
'tenantApplication',
|
||||
'The role for M2M applications that represent a user tenant and send requests to Logto Cloud.'
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
insert into roles_scopes (tenant_id, id, role_id, scope_id)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${generateStandardId()},
|
||||
${roleId},
|
||||
${scopeIds.sms}
|
||||
), (
|
||||
${adminTenantId},
|
||||
${generateStandardId()},
|
||||
${roleId},
|
||||
${scopeIds.email}
|
||||
);
|
||||
`);
|
||||
|
||||
// Insert new scopes to admin role
|
||||
const { id: adminRoleId } = await pool.one<{ id: string }>(sql`
|
||||
select id from roles
|
||||
where name = ${adminRoleName}
|
||||
and tenant_id = ${adminTenantId}
|
||||
`);
|
||||
await pool.query(sql`
|
||||
insert into roles_scopes (tenant_id, id, role_id, scope_id)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${generateStandardId()},
|
||||
${adminRoleId},
|
||||
${scopeIds.sms}
|
||||
), (
|
||||
${adminTenantId},
|
||||
${generateStandardId()},
|
||||
${adminRoleId},
|
||||
${scopeIds.email}
|
||||
);
|
||||
`);
|
||||
|
||||
// Insert m2m applications for each tenant (except admin tenant)
|
||||
const { rows } = await pool.query<{ id: string }>(sql`
|
||||
select id from tenants
|
||||
`);
|
||||
await Promise.all(
|
||||
rows.map(async (row) => {
|
||||
const tenantId = row.id;
|
||||
|
||||
if (tenantId === adminTenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const applicationId = generateStandardId();
|
||||
const description = `Machine to machine application for tenant ${tenantId}`;
|
||||
const oidcClientMetadata = { redirectUris: [], postLogoutRedirectUris: [] };
|
||||
const customClientMetadata = { tenantId };
|
||||
|
||||
await pool.query(sql`
|
||||
insert into applications (tenant_id, id, name, description, secret, type, oidc_client_metadata, custom_client_metadata)
|
||||
values (
|
||||
'admin',
|
||||
${applicationId},
|
||||
'Cloud Service',
|
||||
${description},
|
||||
${generateStandardId()},
|
||||
'MachineToMachine',
|
||||
${JSON.stringify(oidcClientMetadata)},
|
||||
${JSON.stringify(customClientMetadata)}
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
insert into applications_roles (tenant_id, id, role_id, application_id)
|
||||
values (
|
||||
'admin',
|
||||
${generateStandardId()},
|
||||
${roleId},
|
||||
${applicationId}
|
||||
);
|
||||
`);
|
||||
})
|
||||
);
|
||||
},
|
||||
down: async (pool) => {
|
||||
const role = await pool.one<{ id: string }>(sql`
|
||||
select id from roles
|
||||
where tenant_id = ${adminTenantId}
|
||||
and name='tenantApplication'
|
||||
`);
|
||||
const { rows: applications } = await pool.query<{ id: string }>(sql`
|
||||
select application_id as id from applications_roles
|
||||
where tenant_id = ${adminTenantId}
|
||||
and role_id = ${role.id}
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
delete from applications_roles
|
||||
where tenant_id = ${adminTenantId}
|
||||
and role_id = ${role.id};
|
||||
`);
|
||||
await pool.query(sql`
|
||||
delete from roles_scopes
|
||||
where tenant_id = ${adminTenantId}
|
||||
and role_id = ${role.id};
|
||||
`);
|
||||
await pool.query(sql`
|
||||
delete from roles
|
||||
where tenant_id = ${adminTenantId}
|
||||
and id = ${role.id};
|
||||
`);
|
||||
|
||||
if (applications.length > 0) {
|
||||
await pool.query(sql`
|
||||
delete from applications
|
||||
where tenant_id = ${adminTenantId}
|
||||
and id in (${sql.join(
|
||||
applications.map(({ id }) => id),
|
||||
sql`, `
|
||||
)});
|
||||
`);
|
||||
}
|
||||
await pool.query(sql`
|
||||
delete from scopes
|
||||
using resources
|
||||
where resources.id = scopes.resource_id
|
||||
and scopes.tenant_id = ${adminTenantId}
|
||||
and resources.indicator = 'https://cloud.logto.io/api'
|
||||
and (scopes.name='send:sms' or scopes.name='send:email');
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -67,12 +67,14 @@ export enum CustomClientMetadataKey {
|
|||
CorsAllowedOrigins = 'corsAllowedOrigins',
|
||||
IdTokenTtl = 'idTokenTtl',
|
||||
RefreshTokenTtl = 'refreshTokenTtl',
|
||||
TenantId = 'tenantId',
|
||||
}
|
||||
|
||||
export const customClientMetadataGuard = z.object({
|
||||
[CustomClientMetadataKey.CorsAllowedOrigins]: z.string().url().array().optional(),
|
||||
[CustomClientMetadataKey.IdTokenTtl]: z.number().optional(),
|
||||
[CustomClientMetadataKey.RefreshTokenTtl]: z.number().optional(),
|
||||
[CustomClientMetadataKey.TenantId]: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CustomClientMetadata = z.infer<typeof customClientMetadataGuard>;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
|
||||
import type { Application, CreateApplication } from '../db-entries/index.js';
|
||||
import type {
|
||||
Application,
|
||||
CreateApplication,
|
||||
CreateApplicationsRole,
|
||||
} from '../db-entries/index.js';
|
||||
import { ApplicationType } from '../db-entries/index.js';
|
||||
import { adminTenantId } from './tenant.js';
|
||||
|
||||
|
@ -35,3 +39,34 @@ export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplicati
|
|||
type: ApplicationType.SPA,
|
||||
oidcClientMetadata: { redirectUris: [], postLogoutRedirectUris: [] },
|
||||
});
|
||||
|
||||
export const createTenantMachineToMachineApplication = (
|
||||
tenantId: string
|
||||
): Readonly<CreateApplication> =>
|
||||
Object.freeze({
|
||||
tenantId: adminTenantId,
|
||||
id: generateStandardId(),
|
||||
name: 'Cloud Service',
|
||||
description: `Machine to machine application for tenant ${tenantId}`,
|
||||
secret: generateStandardId(),
|
||||
type: ApplicationType.MachineToMachine,
|
||||
oidcClientMetadata: {
|
||||
redirectUris: [],
|
||||
postLogoutRedirectUris: [],
|
||||
},
|
||||
customClientMetadata: {
|
||||
tenantId,
|
||||
},
|
||||
});
|
||||
|
||||
/** Create role for "tenant application (M2M)" in admin tenant */
|
||||
export const createAdminTenantApplicationRole = (
|
||||
applicationId: string,
|
||||
roleId: string
|
||||
): Readonly<CreateApplicationsRole> =>
|
||||
Object.freeze({
|
||||
id: generateStandardId(),
|
||||
tenantId: adminTenantId,
|
||||
applicationId,
|
||||
roleId,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
|
||||
import type { CreateScope } from '../index.js';
|
||||
import type { CreateScope, Role } from '../index.js';
|
||||
import { AdminTenantRole } from '../types/index.js';
|
||||
import type { UpdateAdminData } from './management-api.js';
|
||||
import { adminTenantId } from './tenant.js';
|
||||
|
@ -11,6 +11,8 @@ export const cloudApiIndicator = 'https://cloud.logto.io/api';
|
|||
export enum CloudScope {
|
||||
CreateTenant = 'create:tenant',
|
||||
ManageTenant = 'manage:tenant',
|
||||
SendSms = 'send:sms',
|
||||
SendEmail = 'send:email',
|
||||
}
|
||||
|
||||
export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]> => {
|
||||
|
@ -41,5 +43,21 @@ export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]>
|
|||
CloudScope.ManageTenant,
|
||||
'Allow managing existing tenants, including create without limitation, update, and delete.'
|
||||
),
|
||||
buildScope(
|
||||
CloudScope.SendEmail,
|
||||
'Allow sending emails. This scope is only available to M2M application.'
|
||||
),
|
||||
buildScope(
|
||||
CloudScope.SendSms,
|
||||
'Allow sending SMS. This scope is only available to M2M application.'
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
export const createTenantApplicationRole = (): Readonly<Role> => ({
|
||||
tenantId: adminTenantId,
|
||||
id: generateStandardId(),
|
||||
name: AdminTenantRole.TenantApplication,
|
||||
description:
|
||||
'The role for M2M applications that represent a user tenant and send requests to Logto Cloud.',
|
||||
});
|
||||
|
|
|
@ -36,6 +36,8 @@ export enum AdminTenantRole {
|
|||
Admin = 'admin',
|
||||
/** Common user role in admin tenant. */
|
||||
User = 'user',
|
||||
/** The role for machine to machine applications that represent a user tenant and send requests to Logto Cloud. */
|
||||
TenantApplication = 'tenantApplication',
|
||||
}
|
||||
|
||||
export enum PredefinedScope {
|
||||
|
|
Loading…
Reference in a new issue