mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #5793 from logto-io/yemq-refactor-jwt-library-methods
refactor(core): move the deploy/undeploy worker methods to jwtCustomizerLibrary
This commit is contained in:
commit
012a2c0e36
9 changed files with 148 additions and 137 deletions
|
@ -1,13 +1,29 @@
|
|||
import type { JwtCustomizerUserContext } from '@logto/schemas';
|
||||
import { userInfoSelectFields, jwtCustomizerUserContextGuard } from '@logto/schemas';
|
||||
import { deduplicate, pick, pickState } from '@silverhand/essentials';
|
||||
import {
|
||||
userInfoSelectFields,
|
||||
jwtCustomizerUserContextGuard,
|
||||
type LogtoJwtTokenKey,
|
||||
type JwtCustomizerType,
|
||||
type JwtCustomizerUserContext,
|
||||
} from '@logto/schemas';
|
||||
import { deduplicate, pick, pickState, assert } from '@silverhand/essentials';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type { LogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
||||
import { type ScopeLibrary } from '#src/libraries/scope.js';
|
||||
import { type UserLibrary } from '#src/libraries/user.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import {
|
||||
getJwtCustomizerScripts,
|
||||
type CustomJwtDeployRequestBody,
|
||||
} from '#src/utils/custom-jwt/index.js';
|
||||
|
||||
import { type CloudConnectionLibrary } from './cloud-connection.js';
|
||||
|
||||
export const createJwtCustomizerLibrary = (
|
||||
queries: Queries,
|
||||
logtoConfigs: LogtoConfigLibrary,
|
||||
cloudConnection: CloudConnectionLibrary,
|
||||
userLibrary: UserLibrary,
|
||||
scopeLibrary: ScopeLibrary
|
||||
) => {
|
||||
|
@ -20,6 +36,7 @@ export const createJwtCustomizerLibrary = (
|
|||
} = queries;
|
||||
const { findUserRoles } = userLibrary;
|
||||
const { attachResourceToScopes } = scopeLibrary;
|
||||
const { getJwtCustomizers } = logtoConfigs;
|
||||
|
||||
/**
|
||||
* We does not include org roles' scopes for the following reason:
|
||||
|
@ -65,7 +82,81 @@ export const createJwtCustomizerLibrary = (
|
|||
return jwtCustomizerUserContextGuard.parse(userContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.useCase - The use case of JWT customizer script, can be either `test` or `production`.
|
||||
*/
|
||||
const deployJwtCustomizerScript = async <T extends LogtoJwtTokenKey>(payload: {
|
||||
key: T;
|
||||
value: JwtCustomizerType[T];
|
||||
useCase: 'test' | 'production';
|
||||
}) => {
|
||||
const [client, jwtCustomizers] = await Promise.all([
|
||||
cloudConnection.getClient(),
|
||||
getJwtCustomizers(),
|
||||
]);
|
||||
|
||||
const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers);
|
||||
|
||||
const newCustomizerScripts: CustomJwtDeployRequestBody = {
|
||||
/**
|
||||
* There are at most 4 custom JWT scripts in the `CustomJwtDeployRequestBody`-typed object,
|
||||
* and can be indexed by `data[CustomJwtType][UseCase]`.
|
||||
*
|
||||
* Per our design, each script will be deployed as a API endpoint in the Cloudflare
|
||||
* worker service. A production script will be deployed to `/api/custom-jwt`
|
||||
* endpoint and a test script will be deployed to `/api/custom-jwt/test` endpoint.
|
||||
*
|
||||
* If the current use case is `test`, then the script should be deployed to a `/test` endpoint;
|
||||
* otherwise, the script should be deployed to the `/api/custom-jwt` endpoint and overwrite
|
||||
* previous handler of the API endpoint.
|
||||
*/
|
||||
[payload.key]: { [payload.useCase]: payload.value.script },
|
||||
};
|
||||
|
||||
await client.put(`/api/services/custom-jwt/worker`, {
|
||||
body: deepmerge(customizerScriptsFromDatabase, newCustomizerScripts),
|
||||
});
|
||||
};
|
||||
|
||||
const undeployJwtCustomizerScript = async <T extends LogtoJwtTokenKey>(key: T) => {
|
||||
const [client, jwtCustomizers] = await Promise.all([
|
||||
cloudConnection.getClient(),
|
||||
getJwtCustomizers(),
|
||||
]);
|
||||
|
||||
assert(jwtCustomizers[key], new RequestError({ code: 'entity.not_exists', key }));
|
||||
|
||||
// Undeploy the worker directly if the only JWT customizer is being deleted.
|
||||
if (Object.entries(jwtCustomizers).length === 1) {
|
||||
await client.delete(`/api/services/custom-jwt/worker`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the JWT customizer script (of given `key`) from the existing JWT customizer scripts and redeploy.
|
||||
const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers);
|
||||
const newCustomizerScripts: CustomJwtDeployRequestBody = {
|
||||
[key]: {
|
||||
production: undefined,
|
||||
test: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await client.put(`/api/services/custom-jwt/worker`, {
|
||||
body: deepmerge(customizerScriptsFromDatabase, newCustomizerScripts),
|
||||
});
|
||||
};
|
||||
return {
|
||||
getUserContext,
|
||||
deployJwtCustomizerScript,
|
||||
undeployJwtCustomizerScript,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,20 +8,12 @@ import {
|
|||
jwtCustomizerConfigGuard,
|
||||
logtoOidcConfigGuard,
|
||||
} from '@logto/schemas';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import deepmerge from 'deepmerge';
|
||||
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,
|
||||
type CustomJwtDeployRequestBody,
|
||||
} from '#src/utils/custom-jwt/index.js';
|
||||
|
||||
import { type CloudConnectionLibrary } from './cloud-connection.js';
|
||||
|
||||
export type LogtoConfigLibrary = ReturnType<typeof createLogtoConfigLibrary>;
|
||||
|
||||
|
@ -137,85 +129,6 @@ 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.useCase - The use case of JWT customizer script, can be either `test` or `production`.
|
||||
*/
|
||||
const deployJwtCustomizerScript = async <T extends LogtoJwtTokenKey>(
|
||||
cloudConnection: CloudConnectionLibrary,
|
||||
payload: {
|
||||
key: T;
|
||||
value: JwtCustomizerType[T];
|
||||
useCase: 'test' | 'production';
|
||||
}
|
||||
) => {
|
||||
const [client, jwtCustomizers] = await Promise.all([
|
||||
cloudConnection.getClient(),
|
||||
getJwtCustomizers(),
|
||||
]);
|
||||
|
||||
const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers);
|
||||
|
||||
const newCustomizerScripts: CustomJwtDeployRequestBody = {
|
||||
/**
|
||||
* There are at most 4 custom JWT scripts in the `CustomJwtDeployRequestBody`-typed object,
|
||||
* and can be indexed by `data[CustomJwtType][UseCase]`.
|
||||
*
|
||||
* Per our design, each script will be deployed as a API endpoint in the Cloudflare
|
||||
* worker service. A production script will be deployed to `/api/custom-jwt`
|
||||
* endpoint and a test script will be deployed to `/api/custom-jwt/test` endpoint.
|
||||
*
|
||||
* If the current use case is `test`, then the script should be deployed to a `/test` endpoint;
|
||||
* otherwise, the script should be deployed to the `/api/custom-jwt` endpoint and overwrite
|
||||
* previous handler of the API endpoint.
|
||||
*/
|
||||
[payload.key]: { [payload.useCase]: payload.value.script },
|
||||
};
|
||||
|
||||
await client.put(`/api/services/custom-jwt/worker`, {
|
||||
body: deepmerge(customizerScriptsFromDatabase, newCustomizerScripts),
|
||||
});
|
||||
};
|
||||
|
||||
const undeployJwtCustomizerScript = async <T extends LogtoJwtTokenKey>(
|
||||
cloudConnection: CloudConnectionLibrary,
|
||||
key: T
|
||||
) => {
|
||||
const [client, jwtCustomizers] = await Promise.all([
|
||||
cloudConnection.getClient(),
|
||||
getJwtCustomizers(),
|
||||
]);
|
||||
|
||||
assert(jwtCustomizers[key], new RequestError({ code: 'entity.not_exists', key }));
|
||||
|
||||
// Undeploy the worker directly if the only JWT customizer is being deleted.
|
||||
if (Object.entries(jwtCustomizers).length === 1) {
|
||||
await client.delete(`/api/services/custom-jwt/worker`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the JWT customizer script (of given `key`) from the existing JWT customizer scripts and redeploy.
|
||||
const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers);
|
||||
const newCustomizerScripts: CustomJwtDeployRequestBody = {
|
||||
[key]: {
|
||||
production: undefined,
|
||||
test: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await client.put(`/api/services/custom-jwt/worker`, {
|
||||
body: deepmerge(customizerScriptsFromDatabase, newCustomizerScripts),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getOidcConfigs,
|
||||
getCloudConnectionData,
|
||||
|
@ -223,7 +136,5 @@ export const createLogtoConfigLibrary = ({
|
|||
getJwtCustomizer,
|
||||
getJwtCustomizers,
|
||||
updateJwtCustomizer,
|
||||
deployJwtCustomizerScript,
|
||||
undeployJwtCustomizerScript,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -29,7 +29,12 @@ describe('configs JWT customizer routes', () => {
|
|||
undefined,
|
||||
{ logtoConfigs: logtoConfigQueries },
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
jwtCustomizers: {
|
||||
deployJwtCustomizerScript: jest.fn(),
|
||||
undeployJwtCustomizerScript: jest.fn(),
|
||||
},
|
||||
},
|
||||
mockLogtoConfigsLibrary
|
||||
);
|
||||
|
||||
|
@ -55,14 +60,11 @@ describe('configs JWT customizer routes', () => {
|
|||
.put(`/configs/jwt-customizer/access-token`)
|
||||
.send(mockJwtCustomizerConfigForAccessToken.value);
|
||||
|
||||
expect(mockLogtoConfigsLibrary.deployJwtCustomizerScript).toHaveBeenCalledWith(
|
||||
tenantContext.cloudConnection,
|
||||
{
|
||||
key: LogtoJwtTokenKey.AccessToken,
|
||||
value: mockJwtCustomizerConfigForAccessToken.value,
|
||||
useCase: 'production',
|
||||
}
|
||||
);
|
||||
expect(tenantContext.libraries.jwtCustomizers.deployJwtCustomizerScript).toHaveBeenCalledWith({
|
||||
key: LogtoJwtTokenKey.AccessToken,
|
||||
value: mockJwtCustomizerConfigForAccessToken.value,
|
||||
useCase: 'production',
|
||||
});
|
||||
|
||||
expect(mockLogtoConfigsLibrary.upsertJwtCustomizer).toHaveBeenCalledWith(
|
||||
LogtoJwtTokenKey.AccessToken,
|
||||
|
@ -100,14 +102,11 @@ describe('configs JWT customizer routes', () => {
|
|||
.patch('/configs/jwt-customizer/access-token')
|
||||
.send(mockJwtCustomizerConfigForAccessToken.value);
|
||||
|
||||
expect(mockLogtoConfigsLibrary.deployJwtCustomizerScript).toHaveBeenCalledWith(
|
||||
tenantContext.cloudConnection,
|
||||
{
|
||||
key: LogtoJwtTokenKey.AccessToken,
|
||||
value: mockJwtCustomizerConfigForAccessToken.value,
|
||||
useCase: 'production',
|
||||
}
|
||||
);
|
||||
expect(tenantContext.libraries.jwtCustomizers.deployJwtCustomizerScript).toHaveBeenCalledWith({
|
||||
key: LogtoJwtTokenKey.AccessToken,
|
||||
value: mockJwtCustomizerConfigForAccessToken.value,
|
||||
useCase: 'production',
|
||||
});
|
||||
|
||||
expect(mockLogtoConfigsLibrary.updateJwtCustomizer).toHaveBeenCalledWith(
|
||||
LogtoJwtTokenKey.AccessToken,
|
||||
|
@ -141,8 +140,7 @@ describe('configs JWT customizer routes', () => {
|
|||
|
||||
it('DELETE /configs/jwt-customizer/:tokenType should delete the record', async () => {
|
||||
const response = await routeRequester.delete('/configs/jwt-customizer/client-credentials');
|
||||
expect(mockLogtoConfigsLibrary.undeployJwtCustomizerScript).toHaveBeenCalledWith(
|
||||
tenantContext.cloudConnection,
|
||||
expect(tenantContext.libraries.jwtCustomizers.undeployJwtCustomizerScript).toHaveBeenCalledWith(
|
||||
LogtoJwtTokenKey.ClientCredentials
|
||||
);
|
||||
expect(logtoConfigQueries.deleteJwtCustomizer).toHaveBeenCalledWith(
|
||||
|
@ -165,14 +163,11 @@ describe('configs JWT customizer routes', () => {
|
|||
|
||||
const response = await routeRequester.post('/configs/jwt-customizer/test').send(payload);
|
||||
|
||||
expect(mockLogtoConfigsLibrary.deployJwtCustomizerScript).toHaveBeenCalledWith(
|
||||
tenantContext.cloudConnection,
|
||||
{
|
||||
key: LogtoJwtTokenKey.ClientCredentials,
|
||||
value: payload,
|
||||
useCase: 'test',
|
||||
}
|
||||
);
|
||||
expect(tenantContext.libraries.jwtCustomizers.deployJwtCustomizerScript).toHaveBeenCalledWith({
|
||||
key: LogtoJwtTokenKey.ClientCredentials,
|
||||
value: payload,
|
||||
useCase: 'test',
|
||||
});
|
||||
|
||||
expect(mockCloudClient.post).toHaveBeenCalledWith('/api/services/custom-jwt', {
|
||||
body: payload,
|
||||
|
|
|
@ -38,14 +38,9 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
|||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const { getRowsByKeys, deleteJwtCustomizer } = queries.logtoConfigs;
|
||||
const {
|
||||
upsertJwtCustomizer,
|
||||
getJwtCustomizer,
|
||||
getJwtCustomizers,
|
||||
updateJwtCustomizer,
|
||||
deployJwtCustomizerScript,
|
||||
undeployJwtCustomizerScript,
|
||||
} = logtoConfigs;
|
||||
const { upsertJwtCustomizer, getJwtCustomizer, getJwtCustomizers, updateJwtCustomizer } =
|
||||
logtoConfigs;
|
||||
const { deployJwtCustomizerScript, undeployJwtCustomizerScript } = libraries.jwtCustomizers;
|
||||
|
||||
router.put(
|
||||
'/configs/jwt-customizer/:tokenTypePath',
|
||||
|
@ -83,7 +78,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
|||
|
||||
// Deploy first to avoid the case where the JWT customizer was saved to DB but not deployed successfully.
|
||||
if (!isIntegrationTest) {
|
||||
await deployJwtCustomizerScript(cloudConnection, {
|
||||
await deployJwtCustomizerScript({
|
||||
key,
|
||||
value: body,
|
||||
useCase: 'production',
|
||||
|
@ -127,7 +122,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
|||
|
||||
// Deploy first to avoid the case where the JWT customizer was saved to DB but not deployed successfully.
|
||||
if (!isIntegrationTest) {
|
||||
await deployJwtCustomizerScript(cloudConnection, {
|
||||
await deployJwtCustomizerScript({
|
||||
key,
|
||||
value: body,
|
||||
useCase: 'production',
|
||||
|
@ -199,7 +194,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
|||
|
||||
// Undeploy the script first to avoid the case where the JWT customizer was deleted from DB but worker script not updated successfully.
|
||||
if (!isIntegrationTest) {
|
||||
await undeployJwtCustomizerScript(cloudConnection, tokenKey);
|
||||
await undeployJwtCustomizerScript(tokenKey);
|
||||
}
|
||||
|
||||
await deleteJwtCustomizer(tokenKey);
|
||||
|
@ -224,7 +219,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
|||
const { body } = ctx.guard;
|
||||
|
||||
// Deploy the test script
|
||||
await deployJwtCustomizerScript(cloudConnection, {
|
||||
await deployJwtCustomizerScript({
|
||||
key:
|
||||
body.tokenType === LogtoJwtTokenKeyType.AccessToken
|
||||
? LogtoJwtTokenKey.AccessToken
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { ConnectorLibrary } from '#src/libraries/connector.js';
|
|||
import { createDomainLibrary } from '#src/libraries/domain.js';
|
||||
import { createHookLibrary } from '#src/libraries/hook/index.js';
|
||||
import { createJwtCustomizerLibrary } from '#src/libraries/jwt-customizer.js';
|
||||
import type { LogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
||||
import { OrganizationInvitationLibrary } from '#src/libraries/organization-invitation.js';
|
||||
import { createPasscodeLibrary } from '#src/libraries/passcode.js';
|
||||
import { createPhraseLibrary } from '#src/libraries/phrase.js';
|
||||
|
@ -25,7 +26,14 @@ export default class Libraries {
|
|||
hooks = createHookLibrary(this.queries);
|
||||
scopes = createScopeLibrary(this.queries);
|
||||
socials = createSocialLibrary(this.queries, this.connectors);
|
||||
jwtCustomizers = createJwtCustomizerLibrary(this.queries, this.users, this.scopes);
|
||||
jwtCustomizers = createJwtCustomizerLibrary(
|
||||
this.queries,
|
||||
this.logtoConfigs,
|
||||
this.cloudConnection,
|
||||
this.users,
|
||||
this.scopes
|
||||
);
|
||||
|
||||
passcodes = createPasscodeLibrary(this.queries, this.connectors);
|
||||
applications = createApplicationLibrary(this.queries);
|
||||
verificationStatuses = createVerificationStatusLibrary(this.queries);
|
||||
|
@ -52,6 +60,7 @@ export default class Libraries {
|
|||
private readonly queries: Queries,
|
||||
// Explicitly passing connector library to eliminate dependency issue
|
||||
private readonly connectors: ConnectorLibrary,
|
||||
private readonly cloudConnection: CloudConnectionLibrary
|
||||
private readonly cloudConnection: CloudConnectionLibrary,
|
||||
private readonly logtoConfigs: LogtoConfigLibrary
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,13 @@ export default class Tenant implements TenantContext {
|
|||
public readonly logtoConfigs = createLogtoConfigLibrary(queries),
|
||||
public readonly cloudConnection = createCloudConnectionLibrary(logtoConfigs),
|
||||
public readonly connectors = createConnectorLibrary(queries, cloudConnection),
|
||||
public readonly libraries = new Libraries(id, queries, connectors, cloudConnection),
|
||||
public readonly libraries = new Libraries(
|
||||
id,
|
||||
queries,
|
||||
connectors,
|
||||
cloudConnection,
|
||||
logtoConfigs
|
||||
),
|
||||
public readonly sentinel = new BasicSentinel(envSet.pool)
|
||||
) {
|
||||
const isAdminTenant = id === adminTenantId;
|
||||
|
|
|
@ -12,8 +12,6 @@ export const mockLogtoConfigsLibrary: jest.Mocked<LogtoConfigLibrary> = {
|
|||
getJwtCustomizer: jest.fn(),
|
||||
getJwtCustomizers: jest.fn(),
|
||||
updateJwtCustomizer: jest.fn(),
|
||||
deployJwtCustomizerScript: jest.fn(),
|
||||
undeployJwtCustomizerScript: jest.fn(),
|
||||
};
|
||||
|
||||
export const mockCloudClient = new Client<typeof router>({ baseUrl: 'http://localhost:3001' });
|
||||
|
|
|
@ -84,7 +84,13 @@ export class MockTenant implements TenantContext {
|
|||
...createConnectorLibrary(this.queries, this.cloudConnection),
|
||||
...connectorsOverride,
|
||||
};
|
||||
this.libraries = new Libraries(this.id, this.queries, this.connectors, this.cloudConnection);
|
||||
this.libraries = new Libraries(
|
||||
this.id,
|
||||
this.queries,
|
||||
this.connectors,
|
||||
this.cloudConnection,
|
||||
this.logtoConfigs
|
||||
);
|
||||
this.setPartial('libraries', librariesOverride);
|
||||
this.sentinel = new MockSentinel();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue