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"
|
"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",
|
||||||
|
|
|
@ -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()', () => {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
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
|
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}
|
||||||
|
|
Loading…
Reference in a new issue