From 61c49845a6874c5f5645ca3930413718b610e86f Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 4 Jul 2023 18:14:35 +0800 Subject: [PATCH] refactor(core,schemas,cli): save cloud service m2m app credentials (#4109) --- .../cli/src/commands/database/seed/tables.ts | 11 +++ packages/core/src/env-set/index.ts | 4 +- packages/core/src/libraries/logto-config.ts | 28 +++++- packages/core/src/queries/logto-config.ts | 8 +- packages/core/src/tenants/Libraries.ts | 2 + packages/core/src/utils/endpoint.ts | 13 +++ ...88375200-sync-cloud-m2m-to-logto-config.ts | 85 +++++++++++++++++++ packages/schemas/src/seeds/logto-config.ts | 2 +- 8 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/utils/endpoint.ts create mode 100644 packages/schemas/alterations/next-1688375200-sync-cloud-m2m-to-logto-config.ts diff --git a/packages/cli/src/commands/database/seed/tables.ts b/packages/cli/src/commands/database/seed/tables.ts index ff0a9acdc..f5e1d7390 100644 --- a/packages/cli/src/commands/database/seed/tables.ts +++ b/packages/cli/src/commands/database/seed/tables.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { createDefaultAdminConsoleConfig, + createCloudConnectionConfig, defaultTenantId, adminTenantId, defaultManagementApi, @@ -165,6 +166,16 @@ export const seedTables = async ( await Promise.all([ connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')), + connection.query( + insertInto( + createCloudConnectionConfig( + defaultTenantId, + defaultTenantApplication.id, + defaultTenantApplication.secret + ), + 'logto_configs' + ) + ), connection.query( insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences') ), diff --git a/packages/core/src/env-set/index.ts b/packages/core/src/env-set/index.ts index 1bbe2d6c2..b7dca89d4 100644 --- a/packages/core/src/env-set/index.ts +++ b/packages/core/src/env-set/index.ts @@ -69,7 +69,9 @@ export class EnvSet { this.#pool = pool; - const { getOidcConfigs } = createLogtoConfigLibrary(createLogtoConfigQueries(pool)); + const { getOidcConfigs } = createLogtoConfigLibrary({ + logtoConfigs: createLogtoConfigQueries(pool), + }); const oidcConfigs = await getOidcConfigs(); const endpoint = getTenantEndpoint(this.tenantId, EnvSet.values); diff --git a/packages/core/src/libraries/logto-config.ts b/packages/core/src/libraries/logto-config.ts index 6fdd5e9a6..763b4b9ec 100644 --- a/packages/core/src/libraries/logto-config.ts +++ b/packages/core/src/libraries/logto-config.ts @@ -1,12 +1,19 @@ import type { LogtoOidcConfigType } from '@logto/schemas'; -import { logtoOidcConfigGuard, LogtoOidcConfigKey } from '@logto/schemas'; +import { + cloudApiIndicator, + cloudConnectionDataGuard, + logtoOidcConfigGuard, + LogtoOidcConfigKey, +} from '@logto/schemas'; import chalk from 'chalk'; import { z, ZodError } from 'zod'; import type Queries from '#src/tenants/Queries.js'; import { consoleLog } from '#src/utils/console.js'; -export const createLogtoConfigLibrary = ({ getRowsByKeys }: Queries['logtoConfigs']) => { +export const createLogtoConfigLibrary = ({ + logtoConfigs: { getRowsByKeys, getCloudConnectionData: queryCloudConnectionData }, +}: Pick) => { const getOidcConfigs = async (): Promise => { try { const { rows } = await getRowsByKeys(Object.values(LogtoOidcConfigKey)); @@ -35,5 +42,20 @@ export const createLogtoConfigLibrary = ({ getRowsByKeys }: Queries['logtoConfig } }; - return { getOidcConfigs }; + const getCloudConnectionData = async () => { + const { value } = await queryCloudConnectionData(); + const result = cloudConnectionDataGuard.safeParse(value); + + if (!result.success) { + return; + } + + return { + appId: result.data.appId, + appSecret: result.data.appSecret, + resource: cloudApiIndicator, + }; + }; + + return { getOidcConfigs, getCloudConnectionData }; }; diff --git a/packages/core/src/queries/logto-config.ts b/packages/core/src/queries/logto-config.ts index d29c341ca..636d1a515 100644 --- a/packages/core/src/queries/logto-config.ts +++ b/packages/core/src/queries/logto-config.ts @@ -21,11 +21,17 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => { returning ${fields.value} `); + const getCloudConnectionData = async () => + pool.one>(sql` + select ${fields.value} from ${table} + where ${fields.key} = ${LogtoTenantConfigKey.CloudConnection} + `); + const getRowsByKeys = async (keys: LogtoConfigKey[]) => pool.query(sql` select ${sql.join([fields.key, fields.value], sql`,`)} from ${table} where ${fields.key} in (${sql.join(keys, sql`,`)}) `); - return { getAdminConsoleConfig, updateAdminConsoleConfig, getRowsByKeys }; + return { getAdminConsoleConfig, updateAdminConsoleConfig, getCloudConnectionData, getRowsByKeys }; }; diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts index 7a608b5e2..e67529a58 100644 --- a/packages/core/src/tenants/Libraries.ts +++ b/packages/core/src/tenants/Libraries.ts @@ -2,6 +2,7 @@ import { createApplicationLibrary } from '#src/libraries/application.js'; import type { ConnectorLibrary } from '#src/libraries/connector.js'; import { createDomainLibrary } from '#src/libraries/domain.js'; import { createHookLibrary } from '#src/libraries/hook/index.js'; +import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js'; import { createPasscodeLibrary } from '#src/libraries/passcode.js'; import { createPhraseLibrary } from '#src/libraries/phrase.js'; import { createResourceLibrary } from '#src/libraries/resource.js'; @@ -23,6 +24,7 @@ export default class Libraries { applications = createApplicationLibrary(this.queries); verificationStatuses = createVerificationStatusLibrary(this.queries); domains = createDomainLibrary(this.queries); + logtoConfigs = createLogtoConfigLibrary(this.queries); constructor( public readonly tenantId: string, diff --git a/packages/core/src/utils/endpoint.ts b/packages/core/src/utils/endpoint.ts new file mode 100644 index 000000000..7d3541d19 --- /dev/null +++ b/packages/core/src/utils/endpoint.ts @@ -0,0 +1,13 @@ +import { appendPath } from '@silverhand/essentials'; + +import { EnvSet } from '#src/env-set/index.js'; + +/** Will use this method in upcoming changes. */ +// eslint-disable-next-line import/no-unused-modules +export const getCloudConnectionEndpoints = async () => { + const { cloudUrlSet, adminUrlSet } = EnvSet.values; + return { + tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(), + endpoint: appendPath(cloudUrlSet.endpoint, 'api').toString(), + }; +}; diff --git a/packages/schemas/alterations/next-1688375200-sync-cloud-m2m-to-logto-config.ts b/packages/schemas/alterations/next-1688375200-sync-cloud-m2m-to-logto-config.ts new file mode 100644 index 000000000..1ca6dd1f4 --- /dev/null +++ b/packages/schemas/alterations/next-1688375200-sync-cloud-m2m-to-logto-config.ts @@ -0,0 +1,85 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const adminTenantId = 'admin'; + +const cloudServiceApplicationName = 'Cloud Service'; + +const cloudConnectionResourceIndicator = 'https://cloud.logto.io/api'; + +enum ApplicationType { + Native = 'Native', + SPA = 'SPA', + Traditional = 'Traditional', + MachineToMachine = 'MachineToMachine', +} + +const cloudConnectionConfigKey = 'cloudConnection'; + +type Application = { + tenantId: string; + id: string; + name: string; + secret: string; + description: string; + type: ApplicationType; + oidcClientMetadata: unknown; + customClientMetadata: { + tenantId: string; + }; + createdAt: number; +}; + +type CloudConnectionConfig = { + tenantId: string; + key: string; + value: unknown; +}; + +const alteration: AlterationScript = { + up: async (pool) => { + const rows = await pool.many( + sql`select * from applications where type = ${ApplicationType.MachineToMachine} and tenant_id = ${adminTenantId} and name = ${cloudServiceApplicationName}` + ); + + const { rows: existingCloudConnections } = await pool.query(sql` + select * from logto_configs where key = ${cloudConnectionConfigKey} + `); + const tenantIdsWithExistingRecords = new Set( + existingCloudConnections.map(({ tenantId }) => tenantId) + ); + const filteredRows = rows.filter( + ({ customClientMetadata: { tenantId } }) => !tenantIdsWithExistingRecords.has(tenantId) + ); + + if (filteredRows.length === 0) { + return; + } + + await pool.query(sql` + insert into logto_configs (tenant_id, key, value) values ${sql.join( + filteredRows.map(({ id, secret, customClientMetadata }) => { + const { tenantId } = customClientMetadata; + const cloudConnectionValue = { + appId: id, + appSecret: secret, + resource: cloudConnectionResourceIndicator, + }; + + return sql`(${tenantId}, ${cloudConnectionConfigKey}, ${JSON.stringify( + cloudConnectionValue + )})`; + }), + sql`,` + )} + `); + }, + down: async (pool) => { + await pool.query(sql` + delete from logto_configs where key = ${cloudConnectionConfigKey} + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/seeds/logto-config.ts b/packages/schemas/src/seeds/logto-config.ts index 0088324d0..9c807a969 100644 --- a/packages/schemas/src/seeds/logto-config.ts +++ b/packages/schemas/src/seeds/logto-config.ts @@ -26,7 +26,7 @@ export const createDefaultAdminConsoleConfig = ( }, } satisfies CreateLogtoConfig); -export const createDefaultCloudConnectionConfig = ( +export const createCloudConnectionConfig = ( forTenantId: string, appId: string, appSecret: string