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

refactor(core,schemas): make the jwt customizer script field mandatory (#5696)

* refactor(core,schemas): make the jwt customizer script field mandatory

make the jwt customizer script field mandatory

* fix(schemas): fix the alteration script

fix the alteration script

* fix(schemas): fix ut

fix ut
This commit is contained in:
simeng-li 2024-04-15 10:38:30 +08:00 committed by GitHub
parent c08feeb872
commit 559331d51e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 38 deletions

View file

@ -155,11 +155,10 @@ describe('configs JWT customizer routes', () => {
jest.spyOn(mockCloudClient, 'post').mockResolvedValue(cloudConnectionResponse);
const payload: JwtCustomizerTestRequestBody = {
tokenType: LogtoJwtTokenKeyType.AccessToken,
tokenType: LogtoJwtTokenKeyType.ClientCredentials,
script: mockJwtCustomizerConfigForClientCredentials.value.script,
environmentVariables: mockJwtCustomizerConfigForClientCredentials.value.environmentVariables,
token: {},
script: mockJwtCustomizerConfigForAccessToken.value.script,
environmentVariables: mockJwtCustomizerConfigForAccessToken.value.environmentVariables,
context: mockJwtCustomizerConfigForAccessToken.value.contextSample,
};
const response = await routeRequester.post('/configs/jwt-customizer/test').send(payload);
@ -167,7 +166,7 @@ describe('configs JWT customizer routes', () => {
expect(mockLogtoConfigsLibrary.deployJwtCustomizerScript).toHaveBeenCalledWith(
tenantContext.cloudConnection,
{
key: LogtoJwtTokenKey.AccessToken,
key: LogtoJwtTokenKey.ClientCredentials,
value: payload,
isTest: true,
}

View file

@ -207,11 +207,6 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
router.post(
'/configs/jwt-customizer/test',
koaGuard({
/**
* Early throws when:
* 1. no `script` provided.
* 2. no `tokenSample` provided.
*/
body: jwtCustomizerTestRequestBodyGuard,
response: jsonObjectGuard,
status: [200, 400, 403, 422],

View file

@ -0,0 +1,23 @@
import { sql } from '@silverhand/slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
// We are making the jwt-customizer script field mandatory
// Delete the records in logto_configs where key is jwt.accessToken or jwt.clientCredentials and value jsonb's script field is undefined
up: async (pool) => {
await pool.query(
sql`
delete from logto_configs
where key in ('jwt.accessToken', 'jwt.clientCredentials')
and value->>'script' is null
`
);
},
down: async () => {
// No down script available, this is a non-reversible operation
// It is fine since we have not released this feature yet
},
};
export default alteration;

View file

@ -1,5 +1,5 @@
import { pick } from '@silverhand/essentials';
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import {
accessTokenJwtCustomizerGuard,
@ -7,6 +7,8 @@ import {
} from './jwt-customizer.js';
const allFields = ['script', 'environmentVariables', 'contextSample', 'tokenSample'] as const;
const requiredFields = ['script'] as const;
const optionalFields = ['environmentVariables', 'contextSample', 'tokenSample'] as const;
const testClientCredentialsTokenPayload = {
script: '',
@ -39,14 +41,35 @@ const testAccessTokenPayload = {
};
describe('test token sample guard', () => {
it.each(allFields)('should pass guard with any of the field not specified', (droppedField) => {
it.each(optionalFields)(
'should pass guard with any of the optionalFields not specified',
(droppedField) => {
const resultAccessToken = accessTokenJwtCustomizerGuard.safeParse(
pick(testAccessTokenPayload, ...allFields.filter((field) => field !== droppedField))
);
if (!resultAccessToken.success) {
console.log('resultAccessToken.error', resultAccessToken.error);
}
expect(resultAccessToken.success).toBe(true);
const resultClientCredentials = clientCredentialsJwtCustomizerGuard.safeParse(
pick(
testClientCredentialsTokenPayload,
...allFields.filter((field) => field !== droppedField)
)
);
if (!resultClientCredentials.success) {
console.log('resultClientCredentials.error', resultClientCredentials.error);
}
expect(resultClientCredentials.success).toBe(true);
}
);
it.each(requiredFields)('should throw when required field is not specified', (droppedField) => {
const resultAccessToken = accessTokenJwtCustomizerGuard.safeParse(
pick(testAccessTokenPayload, ...allFields.filter((field) => field !== droppedField))
);
if (!resultAccessToken.success) {
console.log('resultAccessToken.error', resultAccessToken.error);
}
expect(resultAccessToken.success).toBe(true);
expect(resultAccessToken.success).toBe(false);
const resultClientCredentials = clientCredentialsJwtCustomizerGuard.safeParse(
pick(
@ -54,10 +77,7 @@ describe('test token sample guard', () => {
...allFields.filter((field) => field !== droppedField)
)
);
if (!resultClientCredentials.success) {
console.log('resultClientCredentials.error', resultClientCredentials.error);
}
expect(resultClientCredentials.success).toBe(true);
expect(resultClientCredentials.success).toBe(false);
});
it.each(allFields)(

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { Roles, UserSsoIdentities, Organizations } from '../../db-entries/index.js';
import { Organizations, Roles, UserSsoIdentities } from '../../db-entries/index.js';
import { jsonObjectGuard, mfaFactorsGuard } from '../../foundations/index.js';
import { scopeResponseGuard } from '../scope.js';
import { userInfoGuard } from '../user.js';
@ -32,13 +32,11 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;
export const jwtCustomizerGuard = z
.object({
script: z.string(),
environmentVariables: z.record(z.string()),
contextSample: jsonObjectGuard,
})
.partial();
export const jwtCustomizerGuard = z.object({
script: z.string(),
environmentVariables: z.record(z.string()).optional(),
contextSample: jsonObjectGuard.optional(),
});
export const accessTokenJwtCustomizerGuard = jwtCustomizerGuard
.extend({
@ -66,25 +64,21 @@ export enum LogtoJwtTokenKeyType {
/**
* This guard is for the core JWT customizer testing API request body guard.
* Unlike the DB guard
*
* - rename the `tokenSample` to `token` and is required for testing.
* - rename the `contextSample` to `context` and is required for AccessToken testing.
*/
export const jwtCustomizerTestRequestBodyGuard = z.discriminatedUnion('tokenType', [
z.object({
tokenType: z.literal(LogtoJwtTokenKeyType.AccessToken),
...accessTokenJwtCustomizerGuard
.required({
script: true,
})
.pick({ environmentVariables: true, script: true }).shape,
...accessTokenJwtCustomizerGuard.pick({ environmentVariables: true, script: true }).shape,
token: accessTokenJwtCustomizerGuard.required().shape.tokenSample,
context: accessTokenJwtCustomizerGuard.required().shape.contextSample,
}),
z.object({
tokenType: z.literal(LogtoJwtTokenKeyType.ClientCredentials),
...clientCredentialsJwtCustomizerGuard
.required({
script: true,
})
.pick({ environmentVariables: true, script: true }).shape,
...clientCredentialsJwtCustomizerGuard.pick({ environmentVariables: true, script: true }).shape,
token: clientCredentialsJwtCustomizerGuard.required().shape.tokenSample,
}),
]);