0
Fork 0
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:
wangsijie 2023-03-12 09:34:15 +08:00 committed by GitHub
parent 922e3a3a38
commit 1f618a3a10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 322 additions and 2 deletions

View file

@ -176,6 +176,7 @@ const queryDatabaseData = async (database) => {
'id',
'resource_id',
'role_id',
'application_id',
'scope_id',
'created_at',
'updated_at',

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -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.',
});

View file

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