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:
parent
f1414d3329
commit
9b3d4ef75b
8 changed files with 115 additions and 18 deletions
|
@ -92,7 +92,7 @@
|
|||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/cloud": "0.2.5-ab8a489",
|
||||
"@logto/cloud": "0.2.5-749cae5",
|
||||
"@silverhand/eslint-config": "5.0.0",
|
||||
"@silverhand/ts-config": "5.0.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
|
|
|
@ -38,6 +38,7 @@ const logtoConfigs: LogtoConfigLibrary = {
|
|||
getJwtCustomizer: jest.fn(),
|
||||
getJwtCustomizers: jest.fn(),
|
||||
updateJwtCustomizer: jest.fn(),
|
||||
deployJwtCustomizerScript: jest.fn(),
|
||||
};
|
||||
|
||||
describe('getAccessToken()', () => {
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import type { CloudConnectionData, JwtCustomizerType, LogtoOidcConfigType } from '@logto/schemas';
|
||||
import {
|
||||
cloudApiIndicator,
|
||||
cloudConnectionDataGuard,
|
||||
logtoOidcConfigGuard,
|
||||
LogtoOidcConfigKey,
|
||||
jwtCustomizerConfigGuard,
|
||||
LogtoConfigs,
|
||||
LogtoJwtTokenKey,
|
||||
LogtoOidcConfigKey,
|
||||
cloudApiIndicator,
|
||||
cloudConnectionDataGuard,
|
||||
jwtCustomizerConfigGuard,
|
||||
logtoOidcConfigGuard,
|
||||
} from '@logto/schemas';
|
||||
import type { LogtoOidcConfigType, CloudConnectionData, JwtCustomizerType } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import { z, ZodError } from 'zod';
|
||||
import { ZodError, z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type Queries from '#src/tenants/Queries.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>;
|
||||
|
||||
|
@ -129,6 +132,47 @@ export const createLogtoConfigLibrary = ({
|
|||
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 {
|
||||
getOidcConfigs,
|
||||
getCloudConnectionData,
|
||||
|
@ -136,5 +180,6 @@ export const createLogtoConfigLibrary = ({
|
|||
getJwtCustomizer,
|
||||
getJwtCustomizers,
|
||||
updateJwtCustomizer,
|
||||
deployJwtCustomizerScript,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -3,10 +3,10 @@ import { builtInLanguages } from '@logto/phrases-experience';
|
|||
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
socialTarget01,
|
||||
socialTarget02,
|
||||
mockSignInExperience,
|
||||
mockSocialConnectors,
|
||||
socialTarget01,
|
||||
socialTarget02,
|
||||
wellConfiguredSsoConnector,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
@ -61,6 +61,7 @@ const cloudConnection = createCloudConnectionLibrary({
|
|||
getJwtCustomizer: jest.fn(),
|
||||
getJwtCustomizers: jest.fn(),
|
||||
updateJwtCustomizer: jest.fn(),
|
||||
deployJwtCustomizerScript: jest.fn(),
|
||||
});
|
||||
|
||||
const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');
|
||||
|
|
|
@ -4,9 +4,9 @@ import { pick } from '@silverhand/essentials';
|
|||
import Sinon from 'sinon';
|
||||
|
||||
import {
|
||||
mockLogtoConfigRows,
|
||||
mockJwtCustomizerConfigForAccessToken,
|
||||
mockJwtCustomizerConfigForClientCredentials,
|
||||
mockLogtoConfigRows,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
@ -23,6 +23,7 @@ const logtoConfigLibraries = {
|
|||
getJwtCustomizer: jest.fn(),
|
||||
getJwtCustomizers: jest.fn(),
|
||||
updateJwtCustomizer: jest.fn(),
|
||||
deployJwtCustomizerScript: jest.fn(),
|
||||
};
|
||||
|
||||
const settingRoutes = await pickDefault(import('./index.js'));
|
||||
|
@ -52,6 +53,9 @@ describe('configs JWT customizer routes', () => {
|
|||
const response = await routeRequester
|
||||
.put(`/configs/jwt-customizer/access-token`)
|
||||
.send(mockJwtCustomizerConfigForAccessToken.value);
|
||||
|
||||
expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalled();
|
||||
|
||||
expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalledWith(
|
||||
LogtoJwtTokenKey.AccessToken,
|
||||
mockJwtCustomizerConfigForAccessToken.value
|
||||
|
@ -87,6 +91,9 @@ describe('configs JWT customizer routes', () => {
|
|||
const response = await routeRequester
|
||||
.patch('/configs/jwt-customizer/access-token')
|
||||
.send(mockJwtCustomizerConfigForAccessToken.value);
|
||||
|
||||
expect(logtoConfigLibraries.deployJwtCustomizerScript).toHaveBeenCalled();
|
||||
|
||||
expect(logtoConfigLibraries.updateJwtCustomizer).toHaveBeenCalledWith(
|
||||
LogtoJwtTokenKey.AccessToken,
|
||||
mockJwtCustomizerConfigForAccessToken.value
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {
|
||||
accessTokenJwtCustomizerGuard,
|
||||
clientCredentialsJwtCustomizerGuard,
|
||||
LogtoJwtTokenKey,
|
||||
LogtoJwtTokenKeyType,
|
||||
jsonObjectGuard,
|
||||
accessTokenJwtCustomizerGuard,
|
||||
adminTenantId,
|
||||
clientCredentialsJwtCustomizerGuard,
|
||||
jsonObjectGuard,
|
||||
jwtCustomizerConfigsGuard,
|
||||
jwtCustomizerTestRequestBodyGuard,
|
||||
} from '@logto/schemas';
|
||||
|
@ -34,8 +34,13 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
|
|||
...[router, { id: tenantId, queries, logtoConfigs, cloudConnection }]: RouterInitArgs<T>
|
||||
) {
|
||||
const { getRowsByKeys, deleteJwtCustomizer } = queries.logtoConfigs;
|
||||
const { upsertJwtCustomizer, getJwtCustomizer, getJwtCustomizers, updateJwtCustomizer } =
|
||||
logtoConfigs;
|
||||
const {
|
||||
upsertJwtCustomizer,
|
||||
getJwtCustomizer,
|
||||
getJwtCustomizers,
|
||||
updateJwtCustomizer,
|
||||
deployJwtCustomizerScript,
|
||||
} = logtoConfigs;
|
||||
|
||||
router.put(
|
||||
'/configs/jwt-customizer/:tokenTypePath',
|
||||
|
@ -67,8 +72,17 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
|
|||
params: { tokenTypePath },
|
||||
body: rawBody,
|
||||
} = ctx.guard;
|
||||
|
||||
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 jwtCustomizer = await upsertJwtCustomizer(key, body);
|
||||
|
@ -76,6 +90,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
|
|||
if (rows.length === 0) {
|
||||
ctx.status = 201;
|
||||
}
|
||||
|
||||
ctx.body = jwtCustomizer.value;
|
||||
|
||||
return next();
|
||||
|
@ -94,12 +109,22 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
|
|||
status: [200, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { isIntegrationTest } = EnvSet.values;
|
||||
|
||||
const {
|
||||
params: { tokenTypePath },
|
||||
body: rawBody,
|
||||
} = ctx.guard;
|
||||
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);
|
||||
|
||||
return next();
|
||||
|
|
8
packages/core/src/utils/custom-jwt.ts
Normal file
8
packages/core/src/utils/custom-jwt.ts
Normal 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 };
|
||||
};
|
|
@ -3205,8 +3205,8 @@ importers:
|
|||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-ab8a489
|
||||
version: 0.2.5-ab8a489(zod@3.22.4)
|
||||
specifier: 0.2.5-749cae5
|
||||
version: 0.2.5-749cae5(zod@3.22.4)
|
||||
'@silverhand/eslint-config':
|
||||
specifier: 5.0.0
|
||||
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
|
||||
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):
|
||||
resolution: {integrity: sha512-nUD1n2CDe/nu6x4cOhXfJ5VyKKDqkKv+a/u9zSfbIMxIF0nShybd2LiCYJDO0SPuMqLnmlYFg+79KrdPCNvjIQ==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
|
Loading…
Reference in a new issue