0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): add custom jwt worker deploy (#5682)

call custom jwt worker deploy cloud service when upsert new jwt-customizers
This commit is contained in:
simeng-li 2024-04-12 10:39:05 +08:00 committed by GitHub
parent f1414d3329
commit 9b3d4ef75b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 115 additions and 18 deletions

View file

@ -92,7 +92,7 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@logto/cloud": "0.2.5-ab8a489", "@logto/cloud": "0.2.5-749cae5",
"@silverhand/eslint-config": "5.0.0", "@silverhand/eslint-config": "5.0.0",
"@silverhand/ts-config": "5.0.0", "@silverhand/ts-config": "5.0.0",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",

View file

@ -38,6 +38,7 @@ const logtoConfigs: LogtoConfigLibrary = {
getJwtCustomizer: jest.fn(), getJwtCustomizer: jest.fn(),
getJwtCustomizers: jest.fn(), getJwtCustomizers: jest.fn(),
updateJwtCustomizer: jest.fn(), updateJwtCustomizer: jest.fn(),
deployJwtCustomizerScript: jest.fn(),
}; };
describe('getAccessToken()', () => { describe('getAccessToken()', () => {

View file

@ -1,19 +1,22 @@
import type { CloudConnectionData, JwtCustomizerType, LogtoOidcConfigType } from '@logto/schemas';
import { import {
cloudApiIndicator,
cloudConnectionDataGuard,
logtoOidcConfigGuard,
LogtoOidcConfigKey,
jwtCustomizerConfigGuard,
LogtoConfigs, LogtoConfigs,
LogtoJwtTokenKey, LogtoJwtTokenKey,
LogtoOidcConfigKey,
cloudApiIndicator,
cloudConnectionDataGuard,
jwtCustomizerConfigGuard,
logtoOidcConfigGuard,
} from '@logto/schemas'; } from '@logto/schemas';
import type { LogtoOidcConfigType, CloudConnectionData, JwtCustomizerType } from '@logto/schemas';
import chalk from 'chalk'; import chalk from 'chalk';
import { z, ZodError } from 'zod'; import { ZodError, z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js'; import type Queries from '#src/tenants/Queries.js';
import { consoleLog } from '#src/utils/console.js'; import { consoleLog } from '#src/utils/console.js';
import { getJwtCustomizerScripts } from '#src/utils/custom-jwt.js';
import { type CloudConnectionLibrary } from './cloud-connection.js';
export type LogtoConfigLibrary = ReturnType<typeof createLogtoConfigLibrary>; export type LogtoConfigLibrary = ReturnType<typeof createLogtoConfigLibrary>;
@ -129,6 +132,47 @@ export const createLogtoConfigLibrary = ({
return updatedRow.value; return updatedRow.value;
}; };
/**
* This method is used to deploy the give JWT customizer scripts to the cloud worker service.
*
* @remarks Since cloud worker service deploy all the JWT customizer scripts at once,
* and the latest JWT customizer updates needs to be deployed ahead before saving it to the database,
* we need to merge the input payload with the existing JWT customizer scripts.
*
* @params payload - The latest JWT customizer payload needs to be deployed.
* @params payload.key - The tokenType of the JWT customizer.
* @params payload.value - JWT customizer value
* @params payload.isTest - Whether the JWT customizer is for test environment.
*/
const deployJwtCustomizerScript = async <T extends LogtoJwtTokenKey>(
cloudConnection: CloudConnectionLibrary,
payload: {
key: T;
value: JwtCustomizerType[T];
isTest?: boolean;
}
) => {
const [client, jwtCustomizers] = await Promise.all([
cloudConnection.getClient(),
getJwtCustomizers(),
]);
const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers);
const newCustomizerScripts: { [key in LogtoJwtTokenKey]?: string } = {
[payload.key]: payload.value.script,
};
await client.put(`/api/services/custom-jwt/worker`, {
body: {
production: payload.isTest
? customizerScriptsFromDatabase
: { ...customizerScriptsFromDatabase, ...newCustomizerScripts },
test: payload.isTest ? newCustomizerScripts : undefined,
},
});
};
return { return {
getOidcConfigs, getOidcConfigs,
getCloudConnectionData, getCloudConnectionData,
@ -136,5 +180,6 @@ export const createLogtoConfigLibrary = ({
getJwtCustomizer, getJwtCustomizer,
getJwtCustomizers, getJwtCustomizers,
updateJwtCustomizer, updateJwtCustomizer,
deployJwtCustomizerScript,
}; };
}; };

View file

@ -3,10 +3,10 @@ import { builtInLanguages } from '@logto/phrases-experience';
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
import { import {
socialTarget01,
socialTarget02,
mockSignInExperience, mockSignInExperience,
mockSocialConnectors, mockSocialConnectors,
socialTarget01,
socialTarget02,
wellConfiguredSsoConnector, wellConfiguredSsoConnector,
} from '#src/__mocks__/index.js'; } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -61,6 +61,7 @@ const cloudConnection = createCloudConnectionLibrary({
getJwtCustomizer: jest.fn(), getJwtCustomizer: jest.fn(),
getJwtCustomizers: jest.fn(), getJwtCustomizers: jest.fn(),
updateJwtCustomizer: jest.fn(), updateJwtCustomizer: jest.fn(),
deployJwtCustomizerScript: jest.fn(),
}); });
const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors'); const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');

View file

@ -4,9 +4,9 @@ import { pick } from '@silverhand/essentials';
import Sinon from 'sinon'; import Sinon from 'sinon';
import { import {
mockLogtoConfigRows,
mockJwtCustomizerConfigForAccessToken, mockJwtCustomizerConfigForAccessToken,
mockJwtCustomizerConfigForClientCredentials, mockJwtCustomizerConfigForClientCredentials,
mockLogtoConfigRows,
} from '#src/__mocks__/index.js'; } from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js'; import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js'; import { createRequester } from '#src/utils/test-utils.js';
@ -23,6 +23,7 @@ const logtoConfigLibraries = {
getJwtCustomizer: jest.fn(), getJwtCustomizer: jest.fn(),
getJwtCustomizers: jest.fn(), getJwtCustomizers: jest.fn(),
updateJwtCustomizer: jest.fn(), updateJwtCustomizer: jest.fn(),
deployJwtCustomizerScript: jest.fn(),
}; };
const settingRoutes = await pickDefault(import('./index.js')); const settingRoutes = await pickDefault(import('./index.js'));
@ -52,6 +53,9 @@ describe('configs JWT customizer routes', () => {
const response = await routeRequester const response = await routeRequester
.put(`/configs/jwt-customizer/access-token`) .put(`/configs/jwt-customizer/access-token`)
.send(mockJwtCustomizerConfigForAccessToken.value); .send(mockJwtCustomizerConfigForAccessToken.value);
expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalled();
expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalledWith( expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalledWith(
LogtoJwtTokenKey.AccessToken, LogtoJwtTokenKey.AccessToken,
mockJwtCustomizerConfigForAccessToken.value mockJwtCustomizerConfigForAccessToken.value
@ -87,6 +91,9 @@ describe('configs JWT customizer routes', () => {
const response = await routeRequester const response = await routeRequester
.patch('/configs/jwt-customizer/access-token') .patch('/configs/jwt-customizer/access-token')
.send(mockJwtCustomizerConfigForAccessToken.value); .send(mockJwtCustomizerConfigForAccessToken.value);
expect(logtoConfigLibraries.deployJwtCustomizerScript).toHaveBeenCalled();
expect(logtoConfigLibraries.updateJwtCustomizer).toHaveBeenCalledWith( expect(logtoConfigLibraries.updateJwtCustomizer).toHaveBeenCalledWith(
LogtoJwtTokenKey.AccessToken, LogtoJwtTokenKey.AccessToken,
mockJwtCustomizerConfigForAccessToken.value mockJwtCustomizerConfigForAccessToken.value

View file

@ -1,10 +1,10 @@
import { import {
accessTokenJwtCustomizerGuard,
clientCredentialsJwtCustomizerGuard,
LogtoJwtTokenKey, LogtoJwtTokenKey,
LogtoJwtTokenKeyType, LogtoJwtTokenKeyType,
jsonObjectGuard, accessTokenJwtCustomizerGuard,
adminTenantId, adminTenantId,
clientCredentialsJwtCustomizerGuard,
jsonObjectGuard,
jwtCustomizerConfigsGuard, jwtCustomizerConfigsGuard,
jwtCustomizerTestRequestBodyGuard, jwtCustomizerTestRequestBodyGuard,
} from '@logto/schemas'; } from '@logto/schemas';
@ -34,8 +34,13 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
...[router, { id: tenantId, queries, logtoConfigs, cloudConnection }]: RouterInitArgs<T> ...[router, { id: tenantId, queries, logtoConfigs, cloudConnection }]: RouterInitArgs<T>
) { ) {
const { getRowsByKeys, deleteJwtCustomizer } = queries.logtoConfigs; const { getRowsByKeys, deleteJwtCustomizer } = queries.logtoConfigs;
const { upsertJwtCustomizer, getJwtCustomizer, getJwtCustomizers, updateJwtCustomizer } = const {
logtoConfigs; upsertJwtCustomizer,
getJwtCustomizer,
getJwtCustomizers,
updateJwtCustomizer,
deployJwtCustomizerScript,
} = logtoConfigs;
router.put( router.put(
'/configs/jwt-customizer/:tokenTypePath', '/configs/jwt-customizer/:tokenTypePath',
@ -67,8 +72,17 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
params: { tokenTypePath }, params: { tokenTypePath },
body: rawBody, body: rawBody,
} = ctx.guard; } = ctx.guard;
const { key, body } = getJwtTokenKeyAndBody(tokenTypePath, rawBody); const { key, body } = getJwtTokenKeyAndBody(tokenTypePath, rawBody);
// Deploy first to avoid the case where the JWT customizer was saved to DB but not deployed successfully.
if (!isIntegrationTest) {
await deployJwtCustomizerScript(cloudConnection, {
key,
value: body,
});
}
const { rows } = await getRowsByKeys([key]); const { rows } = await getRowsByKeys([key]);
const jwtCustomizer = await upsertJwtCustomizer(key, body); const jwtCustomizer = await upsertJwtCustomizer(key, body);
@ -76,6 +90,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
if (rows.length === 0) { if (rows.length === 0) {
ctx.status = 201; ctx.status = 201;
} }
ctx.body = jwtCustomizer.value; ctx.body = jwtCustomizer.value;
return next(); return next();
@ -94,12 +109,22 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
status: [200, 400, 404], status: [200, 400, 404],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { isIntegrationTest } = EnvSet.values;
const { const {
params: { tokenTypePath }, params: { tokenTypePath },
body: rawBody, body: rawBody,
} = ctx.guard; } = ctx.guard;
const { key, body } = getJwtTokenKeyAndBody(tokenTypePath, rawBody); const { key, body } = getJwtTokenKeyAndBody(tokenTypePath, rawBody);
// Deploy first to avoid the case where the JWT customizer was saved to DB but not deployed successfully.
if (!isIntegrationTest) {
await deployJwtCustomizerScript(cloudConnection, {
key,
value: body,
});
}
ctx.body = await updateJwtCustomizer(key, body); ctx.body = await updateJwtCustomizer(key, body);
return next(); return next();

View file

@ -0,0 +1,8 @@
import { LogtoJwtTokenKey, type JwtCustomizerType } from '@logto/schemas';
export const getJwtCustomizerScripts = (jwtCustomizers: Partial<JwtCustomizerType>) => {
// eslint-disable-next-line no-restricted-syntax -- enable to infer the type using `Object.fromEntries`
return Object.fromEntries(
Object.values(LogtoJwtTokenKey).map((key) => [key, jwtCustomizers[key]?.script])
) as { [key in LogtoJwtTokenKey]?: string };
};

View file

@ -3205,8 +3205,8 @@ importers:
version: 3.22.4 version: 3.22.4
devDependencies: devDependencies:
'@logto/cloud': '@logto/cloud':
specifier: 0.2.5-ab8a489 specifier: 0.2.5-749cae5
version: 0.2.5-ab8a489(zod@3.22.4) version: 0.2.5-749cae5(zod@3.22.4)
'@silverhand/eslint-config': '@silverhand/eslint-config':
specifier: 5.0.0 specifier: 5.0.0
version: 5.0.0(eslint@8.44.0)(prettier@3.0.0)(typescript@5.3.3) version: 5.0.0(eslint@8.44.0)(prettier@3.0.0)(typescript@5.3.3)
@ -7647,6 +7647,16 @@ packages:
jose: 5.2.2 jose: 5.2.2
dev: true dev: true
/@logto/cloud@0.2.5-749cae5(zod@3.22.4):
resolution: {integrity: sha512-QzebHRSBShQwOsKAvYlVd7QF43RlrHOt/nmwrlRNW4F9U0DUEFaeLZujYS56oxjQ49GRdsOPKSQCE97wRB7NNQ==}
engines: {node: ^20.9.0}
dependencies:
'@silverhand/essentials': 2.9.0
'@withtyped/server': 0.13.3(zod@3.22.4)
transitivePeerDependencies:
- zod
dev: true
/@logto/cloud@0.2.5-ab8a489(zod@3.22.4): /@logto/cloud@0.2.5-ab8a489(zod@3.22.4):
resolution: {integrity: sha512-nUD1n2CDe/nu6x4cOhXfJ5VyKKDqkKv+a/u9zSfbIMxIF0nShybd2LiCYJDO0SPuMqLnmlYFg+79KrdPCNvjIQ==} resolution: {integrity: sha512-nUD1n2CDe/nu6x4cOhXfJ5VyKKDqkKv+a/u9zSfbIMxIF0nShybd2LiCYJDO0SPuMqLnmlYFg+79KrdPCNvjIQ==}
engines: {node: ^20.9.0} engines: {node: ^20.9.0}