From 733f092e40637bbf4b226d96195906e2d6005d4e Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 12 Mar 2024 12:32:24 +0800 Subject: [PATCH] chore(schemas): add cloud scope, service log type and API guard --- .../cli/src/commands/database/seed/tables.ts | 7 +- ...223946-add-fetch-custom-jwt-cloud-scope.ts | 92 +++++++++++++++++++ packages/schemas/src/seeds/cloud-api.ts | 9 ++ packages/schemas/src/types/jwt-customizer.ts | 16 +++- .../schemas/src/types/logto-config/index.ts | 2 +- packages/schemas/src/types/service-log.ts | 1 + 6 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 packages/schemas/alterations/next-1710223946-add-fetch-custom-jwt-cloud-scope.ts diff --git a/packages/cli/src/commands/database/seed/tables.ts b/packages/cli/src/commands/database/seed/tables.ts index 33723c080..1135e0f48 100644 --- a/packages/cli/src/commands/database/seed/tables.ts +++ b/packages/cli/src/commands/database/seed/tables.ts @@ -168,7 +168,12 @@ export const seedTables = async ( adminTenantId, applicationRole.id, ...cloudAdditionalScopes - .filter(({ name }) => name === CloudScope.SendSms || name === CloudScope.SendEmail) + .filter( + ({ name }) => + name === CloudScope.SendSms || + name === CloudScope.SendEmail || + name === CloudScope.FetchCustomJwt + ) .map(({ id }) => id) ); diff --git a/packages/schemas/alterations/next-1710223946-add-fetch-custom-jwt-cloud-scope.ts b/packages/schemas/alterations/next-1710223946-add-fetch-custom-jwt-cloud-scope.ts new file mode 100644 index 000000000..000cf9db6 --- /dev/null +++ b/packages/schemas/alterations/next-1710223946-add-fetch-custom-jwt-cloud-scope.ts @@ -0,0 +1,92 @@ +import { generateStandardId } from '@logto/shared/universal'; +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +type Resource = { + tenantId: string; + id: string; + name: string; + indicator: string; + isDefault: boolean; +}; + +type Scope = { + tenantId: string; + id: string; + resourceId: string; + name: string; + description: string; +}; + +type Role = { + tenantId: string; + id: string; + name: string; + description: string; +}; + +const cloudApiIndicator = 'https://cloud.logto.io/api'; + +const cloudConnectionAppRoleName = 'tenantApplication'; + +const adminTenantId = 'admin'; + +const fetchCustomJwtCloudScopeName = 'fetch:custom:jwt'; +const fetchCustomJwtCloudScopeDescription = + 'Allow accessing external resource to execute JWT payload customizer script and fetch the parsed token payload.'; + +const alteration: AlterationScript = { + up: async (pool) => { + // Get the Cloud API resource + const cloudApiResource = await pool.one(sql` + select * from resources + where tenant_id = ${adminTenantId} + and indicator = ${cloudApiIndicator} + `); + + // Get cloud connection application role + const tenantApplicationRole = await pool.one(sql` + select * from roles + where tenant_id = ${adminTenantId} + and name = ${cloudConnectionAppRoleName} and type = 'MachineToMachine' + `); + + // Create the `custom:jwt` scope + const customJwtCloudScope = await pool.one(sql` + insert into scopes (id, tenant_id, resource_id, name, description) + values (${generateStandardId()}, ${adminTenantId}, ${ + cloudApiResource.id + }, ${fetchCustomJwtCloudScopeName}, ${fetchCustomJwtCloudScopeDescription}) + returning *; + `); + + // Assign the `custom:jwt` scope to cloud connection application role + await pool.query(sql` + insert into roles_scopes (id, tenant_id, role_id, scope_id) + values (${generateStandardId()}, ${adminTenantId}, ${tenantApplicationRole.id}, ${ + customJwtCloudScope.id + }); + `); + }, + down: async (pool) => { + // Get the Cloud API resource + const cloudApiResource = await pool.one(sql` + select * from resources + where tenant_id = ${adminTenantId} + and indicator = ${cloudApiIndicator} + `); + + // Remove the `custom:jwt` scope + await pool.query(sql` + delete from scopes + where + tenant_id = ${adminTenantId} and + name = ${fetchCustomJwtCloudScopeName} and + description = ${fetchCustomJwtCloudScopeDescription} and + resource_id = ${cloudApiResource.id} + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/seeds/cloud-api.ts b/packages/schemas/src/seeds/cloud-api.ts index 97dfc6147..3c839e771 100644 --- a/packages/schemas/src/seeds/cloud-api.ts +++ b/packages/schemas/src/seeds/cloud-api.ts @@ -17,6 +17,11 @@ export enum CloudScope { ManageTenantSelf = 'manage:tenant:self', SendSms = 'send:sms', SendEmail = 'send:email', + /** + * The user can access external (independent from Logto instance) resource to run JWT payload customizer + * scripts and fetch the parsed token payload. + */ + FetchCustomJwt = 'fetch:custom:jwt', /** The user can see and manage affiliates, including create, update, and delete. */ ManageAffiliate = 'manage:affiliate', /** The user can create new affiliates and logs. */ @@ -63,6 +68,10 @@ export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]> CloudScope.SendSms, 'Allow sending SMS. This scope is only available to M2M application.' ), + buildScope( + CloudScope.FetchCustomJwt, + 'Allow accessing external resource to execute JWT payload customizer script and fetch the parsed token payload.' + ), buildScope(CloudScope.CreateAffiliate, 'Allow creating new affiliates and logs.'), buildScope( CloudScope.ManageAffiliate, diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts index 04a0f322a..ea28c2827 100644 --- a/packages/schemas/src/types/jwt-customizer.ts +++ b/packages/schemas/src/types/jwt-customizer.ts @@ -8,8 +8,9 @@ import { Scopes, UserSsoIdentities, } from '../db-entries/index.js'; -import { mfaFactorsGuard } from '../foundations/index.js'; +import { mfaFactorsGuard, jsonObjectGuard } from '../foundations/index.js'; +import { jwtCustomizerGuard } from './logto-config/index.js'; import { userInfoGuard } from './user.js'; const organizationDetailGuard = z.object({ @@ -40,3 +41,16 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({ }); export type JwtCustomizerUserContext = z.infer; + +/** + * This guard is for cloud API use (request body guard). + * Since the cloud API will be use by both testing and production, should keep the fields as general as possible. + * The response guard for the cloud API is `jsonObjectGuard` since it extends the `token` with extra claims. + */ +export const customJwtFetcherGuard = jwtCustomizerGuard + .pick({ script: true, envVars: true }) + .required({ script: true }) + .extend({ + token: jsonObjectGuard, + context: jsonObjectGuard.optional(), + }); diff --git a/packages/schemas/src/types/logto-config/index.ts b/packages/schemas/src/types/logto-config/index.ts index 0674d82bd..a03bf33e4 100644 --- a/packages/schemas/src/types/logto-config/index.ts +++ b/packages/schemas/src/types/logto-config/index.ts @@ -56,7 +56,7 @@ export enum LogtoJwtTokenKey { ClientCredentials = 'jwt.clientCredentials', } -const jwtCustomizerGuard = z +export const jwtCustomizerGuard = z .object({ script: z.string(), envVars: z.record(z.string()), diff --git a/packages/schemas/src/types/service-log.ts b/packages/schemas/src/types/service-log.ts index 48d0298ca..fe34c3a13 100644 --- a/packages/schemas/src/types/service-log.ts +++ b/packages/schemas/src/types/service-log.ts @@ -1,4 +1,5 @@ export enum ServiceLogType { SendEmail = 'sendEmail', SendSms = 'sendSms', + FetchCustomJwt = 'fetchCustomJwt', }