From 0d83cb1fd624d3691df3bd9bae8d626fb63de253 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Mon, 25 Nov 2024 14:00:38 +0800 Subject: [PATCH 1/2] feat: add SAML app secret related APIs --- .../libraries/saml-applications.ts | 3 +- .../src/saml-applications/queries/secrets.ts | 36 +++ .../routes/index.openapi.json | 273 ++++++++++++++++++ .../src/saml-applications/routes/index.ts | 89 ++++++ .../src/api/saml-application.ts | 14 + .../api/application/saml-application.test.ts | 46 +++ .../src/locales/en/errors/application.ts | 1 + .../schemas/src/types/saml-application.ts | 8 + 8 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/saml-applications/routes/index.openapi.json diff --git a/packages/core/src/saml-applications/libraries/saml-applications.ts b/packages/core/src/saml-applications/libraries/saml-applications.ts index 8d4257efd..4e9e613ce 100644 --- a/packages/core/src/saml-applications/libraries/saml-applications.ts +++ b/packages/core/src/saml-applications/libraries/saml-applications.ts @@ -2,6 +2,7 @@ import { ApplicationType, type SamlApplicationResponse, type PatchSamlApplication, + type SamlApplicationSecret, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { removeUndefinedKeys } from '@silverhand/essentials'; @@ -26,7 +27,7 @@ export const createSamlApplicationsLibrary = (queries: Queries) => { applicationId: string, // Set certificate life span to 1 year by default. lifeSpanInDays = 365 - ) => { + ): Promise => { const { privateKey, certificate, notAfter } = await generateKeyPairAndCertificate( lifeSpanInDays ); diff --git a/packages/core/src/saml-applications/queries/secrets.ts b/packages/core/src/saml-applications/queries/secrets.ts index 7101b59be..768568ed4 100644 --- a/packages/core/src/saml-applications/queries/secrets.ts +++ b/packages/core/src/saml-applications/queries/secrets.ts @@ -20,6 +20,13 @@ export const createSamlApplicationSecretsQueries = (pool: CommonQueryMethods) => where ${fields.applicationId}=${applicationId} `); + const findSamlApplicationSecretByApplicationIdAndId = async (applicationId: string, id: string) => + pool.one(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.applicationId} = ${applicationId} and ${fields.id} = ${id} + `); + const deleteSamlApplicationSecretById = async (id: string) => { const { rowCount } = await pool.query(sql` delete from ${table} @@ -31,9 +38,38 @@ export const createSamlApplicationSecretsQueries = (pool: CommonQueryMethods) => } }; + const updateSamlApplicationSecretStatusByApplicationIdAndSecretId = async ( + applicationId: string, + secretId: string, + active: boolean + ) => { + return pool.transaction(async (transaction) => { + if (active) { + // If activating this secret, first deactivate all other secrets of the same application + await transaction.query(sql` + update ${table} + set ${fields.active} = false + where ${fields.applicationId} = ${applicationId} + `); + } + + // Update the status of the specified secret + const updatedSecret = await transaction.one(sql` + update ${table} + set ${fields.active} = ${active} + where ${fields.id} = ${secretId} and ${fields.applicationId} = ${applicationId} + returning ${sql.join(Object.values(fields), sql`, `)} + `); + + return updatedSecret; + }); + }; + return { insertSamlApplicationSecret, findSamlApplicationSecretsByApplicationId, + findSamlApplicationSecretByApplicationIdAndId, deleteSamlApplicationSecretById, + updateSamlApplicationSecretStatusByApplicationIdAndSecretId, }; }; diff --git a/packages/core/src/saml-applications/routes/index.openapi.json b/packages/core/src/saml-applications/routes/index.openapi.json new file mode 100644 index 000000000..9c4f7bcf3 --- /dev/null +++ b/packages/core/src/saml-applications/routes/index.openapi.json @@ -0,0 +1,273 @@ +{ + "tags": [ + { + "name": "SAML applications", + "description": "SAML applications enable Single Sign-On (SSO) integration between Logto (acting as Identity Provider/IdP) and third-party Service Providers (SP) using the SAML 2.0 protocol. These endpoints allow you to manage SAML application configurations." + }, + { + "name": "Dev feature" + } + ], + "paths": { + "/api/saml-applications": { + "post": { + "summary": "Create SAML application", + "description": "Create a new SAML application with the given configuration. This will create both the application entity and its SAML-specific configurations.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the SAML application." + }, + "description": { + "type": "string", + "description": "The description of the SAML application." + }, + "customData": { + "type": "object", + "description": "Custom data for the application." + }, + "config": { + "type": "object", + "properties": { + "attributeMapping": { + "type": "object", + "description": "Mapping of SAML attributes to Logto user properties." + }, + "entityId": { + "type": "string", + "description": "Service provider's entityId." + }, + "acsUrl": { + "type": "object", + "description": "Service provider assertion consumer service URL configuration." + } + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The SAML application was created successfully." + }, + "400": { + "description": "Invalid request body or SAML configuration." + } + } + } + }, + "/api/saml-applications/{id}": { + "get": { + "summary": "Get SAML application", + "description": "Get a SAML application by ID. This will return both the application entity and its SAML-specific configurations.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the SAML application to retrieve." + } + ], + "responses": { + "200": { + "description": "The SAML application was retrieved successfully." + }, + "400": { + "description": "Invalid application ID, the application is not a SAML application." + }, + "404": { + "description": "The SAML application was not found." + } + } + }, + "patch": { + "summary": "Update SAML application", + "description": "Update a SAML application by ID. This will update both the application entity and its SAML-specific configurations.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the SAML application to update." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the SAML application." + }, + "description": { + "type": "string", + "description": "The description of the SAML application." + }, + "customData": { + "type": "object", + "description": "Custom data for the application." + }, + "config": { + "type": "object", + "properties": { + "attributeMapping": { + "type": "object", + "description": "Mapping of SAML attributes to Logto user properties." + }, + "entityId": { + "type": "string", + "description": "Service provider's entityId." + }, + "acsUrl": { + "type": "object", + "description": "Service provider assertion consumer service URL configuration." + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The SAML application was updated successfully." + }, + "400": { + "description": "Invalid application ID or request body." + }, + "404": { + "description": "The SAML application was not found." + }, + "422": { + "description": "Invalid SAML configuration." + } + } + }, + "delete": { + "summary": "Delete SAML application", + "description": "Delete a SAML application by ID. This will remove both the application entity and its SAML-specific configurations.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the SAML application to delete." + } + ], + "responses": { + "204": { + "description": "The SAML application was deleted successfully." + }, + "400": { + "description": "Invalid application ID, the application is not a SAML application." + }, + "404": { + "description": "The SAML application was not found." + } + } + } + }, + "/api/saml-applications/{id}/secrets": { + "post": { + "summary": "Create SAML application secret", + "description": "Create a new secret for the specified SAML application.", + "parameters": [], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "properties": { + "lifeSpanInDays": { + "type": "number", + "description": "The lifespan of the secret in days." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The SAML application secret was created successfully." + }, + "400": { + "description": "Invalid application ID or request body." + }, + "404": { + "description": "The SAML application was not found." + } + } + } + }, + "/api/saml-applications/{id}/secrets/{secretId}": { + "delete": { + "summary": "Delete SAML application secret", + "description": "Delete a secret from the specified SAML application. Active secrets cannot be deleted.", + "parameters": [], + "responses": { + "204": { + "description": "The SAML application secret was deleted successfully." + }, + "400": { + "description": "Invalid application ID or secret ID, or attempting to delete an active secret." + }, + "404": { + "description": "The SAML application or secret was not found." + } + } + }, + "patch": { + "summary": "Update SAML application secret status", + "description": "Update the active status of a SAML application secret.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "active": { + "required": true, + "type": "boolean", + "description": "The new active status of the secret." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The SAML application secret was updated successfully." + }, + "400": { + "description": "Invalid application ID, secret ID or request body." + }, + "404": { + "description": "The SAML application or secret was not found." + } + } + } + } + } +} diff --git a/packages/core/src/saml-applications/routes/index.ts b/packages/core/src/saml-applications/routes/index.ts index ed3256520..24e6b033b 100644 --- a/packages/core/src/saml-applications/routes/index.ts +++ b/packages/core/src/saml-applications/routes/index.ts @@ -3,6 +3,8 @@ import { samlApplicationCreateGuard, samlApplicationPatchGuard, samlApplicationResponseGuard, + samlApplicationSecretResponseGuard, + SamlApplicationSecrets, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { removeUndefinedKeys } from '@silverhand/essentials'; @@ -23,6 +25,11 @@ export default function samlApplicationRoutes( const { applications: { insertApplication, findApplicationById, deleteApplicationById }, samlApplicationConfigs: { insertSamlApplicationConfig }, + samlApplicationSecrets: { + deleteSamlApplicationSecretById, + findSamlApplicationSecretByApplicationIdAndId, + updateSamlApplicationSecretStatusByApplicationIdAndSecretId, + }, } = queries; const { samlApplications: { @@ -145,4 +152,86 @@ export default function samlApplicationRoutes( return next(); } ); + + router.post( + '/saml-applications/:id/secrets', + koaGuard({ + params: z.object({ id: z.string() }), + body: z.object({ lifeSpanInDays: z.number().optional() }), + response: samlApplicationSecretResponseGuard, + status: [201, 400, 404], + }), + async (ctx, next) => { + const { + body: { lifeSpanInDays }, + params: { id }, + } = ctx.guard; + + ctx.status = 201; + ctx.body = await createNewSamlApplicationSecretForApplication(id, lifeSpanInDays); + + return next(); + } + ); + + router.delete( + '/saml-applications/:id/secrets/:secretId', + koaGuard({ + params: z.object({ id: z.string(), secretId: z.string() }), + status: [204, 400, 404], + }), + async (ctx, next) => { + const { id, secretId } = ctx.guard.params; + + // Although we can directly find the SAML app secret by `secretId` here, to prevent deleting a secret that does not belong to the current application, we will first verify through the application ID and secret ID. + const samlApplicationSecret = await findSamlApplicationSecretByApplicationIdAndId( + id, + secretId + ); + + assertThat(!samlApplicationSecret.active, 'application.saml.can_not_delete_active_secret'); + + await deleteSamlApplicationSecretById(secretId); + + ctx.status = 204; + + return next(); + } + ); + + router.patch( + '/saml-applications/:id/secrets/:secretId', + koaGuard({ + params: z.object({ id: z.string(), secretId: z.string() }), + body: SamlApplicationSecrets.createGuard.pick({ + active: true, + }), + response: samlApplicationSecretResponseGuard, + status: [200, 400, 404], + }), + async (ctx, next) => { + const { id, secretId } = ctx.guard.params; + const { active } = ctx.guard.body; + + const originalSamlApplicationSecret = await findSamlApplicationSecretByApplicationIdAndId( + id, + secretId + ); + + if (originalSamlApplicationSecret.active === active) { + ctx.status = 200; + ctx.body = originalSamlApplicationSecret; + + return next(); + } + + const updatedSamlApplicationSecret = + await updateSamlApplicationSecretStatusByApplicationIdAndSecretId(id, secretId, active); + + ctx.status = 200; + ctx.body = updatedSamlApplicationSecret; + + return next(); + } + ); } diff --git a/packages/integration-tests/src/api/saml-application.ts b/packages/integration-tests/src/api/saml-application.ts index c452a177d..8d83a871c 100644 --- a/packages/integration-tests/src/api/saml-application.ts +++ b/packages/integration-tests/src/api/saml-application.ts @@ -2,6 +2,7 @@ import { type SamlApplicationResponse, type CreateSamlApplication, type PatchSamlApplication, + type SamlApplicationSecretResponse, } from '@logto/schemas'; import { authedAdminApi } from './api.js'; @@ -26,3 +27,16 @@ export const updateSamlApplication = async ( export const getSamlApplication = async (id: string) => authedAdminApi.get(`saml-applications/${id}`).json(); + +export const createSamlApplicationSecret = async (id: string, lifeSpanInDays: number) => + authedAdminApi + .post(`saml-applications/${id}/secrets`, { json: { lifeSpanInDays } }) + .json(); + +export const deleteSamlApplicationSecret = async (id: string, secretId: string) => + authedAdminApi.delete(`saml-applications/${id}/secrets/${secretId}`); + +export const updateSamlApplicationSecret = async (id: string, secretId: string, active: boolean) => + authedAdminApi + .patch(`saml-applications/${id}/secrets/${secretId}`, { json: { active } }) + .json(); diff --git a/packages/integration-tests/src/tests/api/application/saml-application.test.ts b/packages/integration-tests/src/tests/api/application/saml-application.test.ts index 08a4eaf95..2fd7c8472 100644 --- a/packages/integration-tests/src/tests/api/application/saml-application.test.ts +++ b/packages/integration-tests/src/tests/api/application/saml-application.test.ts @@ -6,6 +6,9 @@ import { deleteSamlApplication, updateSamlApplication, getSamlApplication, + deleteSamlApplicationSecret, + createSamlApplicationSecret, + updateSamlApplicationSecret, } from '#src/api/saml-application.js'; import { expectRejects } from '#src/helpers/index.js'; import { devFeatureTest } from '#src/utils.js'; @@ -103,4 +106,47 @@ describe('SAML application', () => { }); await deleteApplication(application.id); }); + + it('should be able to create and delete SAML application secrets', async () => { + const { id } = await createSamlApplication({ + name: 'test', + description: 'test', + }); + + const createdSecret = await createSamlApplicationSecret(id, 30); + const samlApplication = await getSamlApplication(id); + expect(samlApplication.secrets.length).toBe(2); + expect( + samlApplication.secrets.find((secret) => secret.id === createdSecret.id) + ).not.toBeUndefined(); + + await deleteSamlApplicationSecret(id, createdSecret.id); + + await deleteSamlApplication(id); + }); + + it('should be able to update SAML application secret status and can not delete active secret', async () => { + const { id } = await createSamlApplication({ + name: 'test', + description: 'test', + }); + + const createdSecret = await createSamlApplicationSecret(id, 30); + const samlApplication = await getSamlApplication(id); + expect(samlApplication.secrets.length).toBe(2); + expect( + samlApplication.secrets.find((secret) => secret.id === createdSecret.id) + ).not.toBeUndefined(); + + const updatedSecret = await updateSamlApplicationSecret(id, createdSecret.id, true); + expect(updatedSecret.active).toBe(true); + + await expectRejects(deleteSamlApplicationSecret(id, createdSecret.id), { + code: 'application.saml.can_not_delete_active_secret', + status: 400, + }); + + await updateSamlApplicationSecret(id, createdSecret.id, false); + await deleteSamlApplication(id); + }); }); diff --git a/packages/phrases/src/locales/en/errors/application.ts b/packages/phrases/src/locales/en/errors/application.ts index 7a4dc2960..f79b65af9 100644 --- a/packages/phrases/src/locales/en/errors/application.ts +++ b/packages/phrases/src/locales/en/errors/application.ts @@ -25,6 +25,7 @@ const application = { saml_application_only: 'The API is only available for SAML applications.', acs_url_binding_not_supported: 'Only HTTP-POST binding is supported for receiving SAML assertions.', + can_not_delete_active_secret: 'Can not delete the active secret.', }, }; diff --git a/packages/schemas/src/types/saml-application.ts b/packages/schemas/src/types/saml-application.ts index 98f786a7a..e78058bbe 100644 --- a/packages/schemas/src/types/saml-application.ts +++ b/packages/schemas/src/types/saml-application.ts @@ -37,6 +37,14 @@ export const samlApplicationPatchGuard = applicationPatchGuard export type PatchSamlApplication = z.infer; +export const samlApplicationSecretResponseGuard = SamlApplicationSecrets.guard.omit({ + tenantId: true, + applicationId: true, + privateKey: true, +}); + +export type SamlApplicationSecretResponse = z.infer; + export const samlApplicationResponseGuard = Applications.guard.merge( // Partial to allow the optional fields to be omitted in the response. // When starting to create a SAML application, SAML configuration is optional, which can lead to the absence of SAML configuration. From 79eeb3508ca8df2bc726090b9aab8ae887f18ebb Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 28 Nov 2024 12:52:52 +0800 Subject: [PATCH 2/2] chore: fix code --- .../routes/index.openapi.json | 273 ------------------ .../src/saml-applications/routes/index.ts | 20 +- .../src/api/saml-application.ts | 3 + .../api/application/saml-application.test.ts | 29 +- .../schemas/src/types/saml-application.ts | 2 + 5 files changed, 47 insertions(+), 280 deletions(-) delete mode 100644 packages/core/src/saml-applications/routes/index.openapi.json diff --git a/packages/core/src/saml-applications/routes/index.openapi.json b/packages/core/src/saml-applications/routes/index.openapi.json deleted file mode 100644 index 9c4f7bcf3..000000000 --- a/packages/core/src/saml-applications/routes/index.openapi.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "tags": [ - { - "name": "SAML applications", - "description": "SAML applications enable Single Sign-On (SSO) integration between Logto (acting as Identity Provider/IdP) and third-party Service Providers (SP) using the SAML 2.0 protocol. These endpoints allow you to manage SAML application configurations." - }, - { - "name": "Dev feature" - } - ], - "paths": { - "/api/saml-applications": { - "post": { - "summary": "Create SAML application", - "description": "Create a new SAML application with the given configuration. This will create both the application entity and its SAML-specific configurations.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "properties": { - "name": { - "type": "string", - "description": "The name of the SAML application." - }, - "description": { - "type": "string", - "description": "The description of the SAML application." - }, - "customData": { - "type": "object", - "description": "Custom data for the application." - }, - "config": { - "type": "object", - "properties": { - "attributeMapping": { - "type": "object", - "description": "Mapping of SAML attributes to Logto user properties." - }, - "entityId": { - "type": "string", - "description": "Service provider's entityId." - }, - "acsUrl": { - "type": "object", - "description": "Service provider assertion consumer service URL configuration." - } - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "The SAML application was created successfully." - }, - "400": { - "description": "Invalid request body or SAML configuration." - } - } - } - }, - "/api/saml-applications/{id}": { - "get": { - "summary": "Get SAML application", - "description": "Get a SAML application by ID. This will return both the application entity and its SAML-specific configurations.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the SAML application to retrieve." - } - ], - "responses": { - "200": { - "description": "The SAML application was retrieved successfully." - }, - "400": { - "description": "Invalid application ID, the application is not a SAML application." - }, - "404": { - "description": "The SAML application was not found." - } - } - }, - "patch": { - "summary": "Update SAML application", - "description": "Update a SAML application by ID. This will update both the application entity and its SAML-specific configurations.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the SAML application to update." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "properties": { - "name": { - "type": "string", - "description": "The name of the SAML application." - }, - "description": { - "type": "string", - "description": "The description of the SAML application." - }, - "customData": { - "type": "object", - "description": "Custom data for the application." - }, - "config": { - "type": "object", - "properties": { - "attributeMapping": { - "type": "object", - "description": "Mapping of SAML attributes to Logto user properties." - }, - "entityId": { - "type": "string", - "description": "Service provider's entityId." - }, - "acsUrl": { - "type": "object", - "description": "Service provider assertion consumer service URL configuration." - } - } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "The SAML application was updated successfully." - }, - "400": { - "description": "Invalid application ID or request body." - }, - "404": { - "description": "The SAML application was not found." - }, - "422": { - "description": "Invalid SAML configuration." - } - } - }, - "delete": { - "summary": "Delete SAML application", - "description": "Delete a SAML application by ID. This will remove both the application entity and its SAML-specific configurations.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the SAML application to delete." - } - ], - "responses": { - "204": { - "description": "The SAML application was deleted successfully." - }, - "400": { - "description": "Invalid application ID, the application is not a SAML application." - }, - "404": { - "description": "The SAML application was not found." - } - } - } - }, - "/api/saml-applications/{id}/secrets": { - "post": { - "summary": "Create SAML application secret", - "description": "Create a new secret for the specified SAML application.", - "parameters": [], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "properties": { - "lifeSpanInDays": { - "type": "number", - "description": "The lifespan of the secret in days." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "The SAML application secret was created successfully." - }, - "400": { - "description": "Invalid application ID or request body." - }, - "404": { - "description": "The SAML application was not found." - } - } - } - }, - "/api/saml-applications/{id}/secrets/{secretId}": { - "delete": { - "summary": "Delete SAML application secret", - "description": "Delete a secret from the specified SAML application. Active secrets cannot be deleted.", - "parameters": [], - "responses": { - "204": { - "description": "The SAML application secret was deleted successfully." - }, - "400": { - "description": "Invalid application ID or secret ID, or attempting to delete an active secret." - }, - "404": { - "description": "The SAML application or secret was not found." - } - } - }, - "patch": { - "summary": "Update SAML application secret status", - "description": "Update the active status of a SAML application secret.", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "properties": { - "active": { - "required": true, - "type": "boolean", - "description": "The new active status of the secret." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "The SAML application secret was updated successfully." - }, - "400": { - "description": "Invalid application ID, secret ID or request body." - }, - "404": { - "description": "The SAML application or secret was not found." - } - } - } - } - } -} diff --git a/packages/core/src/saml-applications/routes/index.ts b/packages/core/src/saml-applications/routes/index.ts index 24e6b033b..1d7e0366c 100644 --- a/packages/core/src/saml-applications/routes/index.ts +++ b/packages/core/src/saml-applications/routes/index.ts @@ -27,6 +27,7 @@ export default function samlApplicationRoutes( samlApplicationConfigs: { insertSamlApplicationConfig }, samlApplicationSecrets: { deleteSamlApplicationSecretById, + findSamlApplicationSecretsByApplicationId, findSamlApplicationSecretByApplicationIdAndId, updateSamlApplicationSecretStatusByApplicationIdAndSecretId, }, @@ -168,7 +169,24 @@ export default function samlApplicationRoutes( } = ctx.guard; ctx.status = 201; - ctx.body = await createNewSamlApplicationSecretForApplication(id, lifeSpanInDays); + ctx.body = await createSamlApplicationSecret(id, lifeSpanInDays); + + return next(); + } + ); + + router.get( + '/saml-applications/:id/secrets', + koaGuard({ + params: z.object({ id: z.string() }), + response: samlApplicationSecretResponseGuard.array(), + status: [200, 400, 404], + }), + async (ctx, next) => { + const { id } = ctx.guard.params; + + ctx.status = 200; + ctx.body = await findSamlApplicationSecretsByApplicationId(id); return next(); } diff --git a/packages/integration-tests/src/api/saml-application.ts b/packages/integration-tests/src/api/saml-application.ts index 8d83a871c..33e1b9a6e 100644 --- a/packages/integration-tests/src/api/saml-application.ts +++ b/packages/integration-tests/src/api/saml-application.ts @@ -33,6 +33,9 @@ export const createSamlApplicationSecret = async (id: string, lifeSpanInDays: nu .post(`saml-applications/${id}/secrets`, { json: { lifeSpanInDays } }) .json(); +export const getSamlApplicationSecrets = async (id: string) => + authedAdminApi.get(`saml-applications/${id}/secrets`).json(); + export const deleteSamlApplicationSecret = async (id: string, secretId: string) => authedAdminApi.delete(`saml-applications/${id}/secrets/${secretId}`); diff --git a/packages/integration-tests/src/tests/api/application/saml-application.test.ts b/packages/integration-tests/src/tests/api/application/saml-application.test.ts index 2fd7c8472..23eead64d 100644 --- a/packages/integration-tests/src/tests/api/application/saml-application.test.ts +++ b/packages/integration-tests/src/tests/api/application/saml-application.test.ts @@ -9,6 +9,7 @@ import { deleteSamlApplicationSecret, createSamlApplicationSecret, updateSamlApplicationSecret, + getSamlApplicationSecrets, } from '#src/api/saml-application.js'; import { expectRejects } from '#src/helpers/index.js'; import { devFeatureTest } from '#src/utils.js'; @@ -114,11 +115,17 @@ describe('SAML application', () => { }); const createdSecret = await createSamlApplicationSecret(id, 30); - const samlApplication = await getSamlApplication(id); - expect(samlApplication.secrets.length).toBe(2); + + // @ts-expect-error - Make sure the `privateKey` is not exposed in the response. + expect(createdSecret.privateKey).toBeUndefined(); + + const samlApplicationSecrets = await getSamlApplicationSecrets(id); + expect(samlApplicationSecrets.length).toBe(2); expect( - samlApplication.secrets.find((secret) => secret.id === createdSecret.id) + samlApplicationSecrets.find((secret) => secret.id === createdSecret.id) ).not.toBeUndefined(); + // @ts-expect-error - Make sure the `privateKey` is not exposed in the response. + expect(samlApplicationSecrets.every((secret) => secret.privateKey === undefined)).toBe(true); await deleteSamlApplicationSecret(id, createdSecret.id); @@ -132,14 +139,23 @@ describe('SAML application', () => { }); const createdSecret = await createSamlApplicationSecret(id, 30); - const samlApplication = await getSamlApplication(id); - expect(samlApplication.secrets.length).toBe(2); + + // @ts-expect-error - Make sure the `privateKey` is not exposed in the response. + expect(createdSecret.privateKey).toBeUndefined(); + + const samlApplicationSecrets = await getSamlApplicationSecrets(id); + expect(samlApplicationSecrets.length).toBe(2); expect( - samlApplication.secrets.find((secret) => secret.id === createdSecret.id) + samlApplicationSecrets.find((secret) => secret.id === createdSecret.id) ).not.toBeUndefined(); + // @ts-expect-error - Make sure the `privateKey` is not exposed in the response. + expect(samlApplicationSecrets.every((secret) => secret.privateKey === undefined)).toBe(true); + const updatedSecret = await updateSamlApplicationSecret(id, createdSecret.id, true); expect(updatedSecret.active).toBe(true); + // @ts-expect-error - Make sure the `privateKey` is not exposed in the response. + expect(updatedSecret.privateKey).toBeUndefined(); await expectRejects(deleteSamlApplicationSecret(id, createdSecret.id), { code: 'application.saml.can_not_delete_active_secret', @@ -147,6 +163,7 @@ describe('SAML application', () => { }); await updateSamlApplicationSecret(id, createdSecret.id, false); + await deleteSamlApplicationSecret(id, createdSecret.id); await deleteSamlApplication(id); }); }); diff --git a/packages/schemas/src/types/saml-application.ts b/packages/schemas/src/types/saml-application.ts index e78058bbe..a098541be 100644 --- a/packages/schemas/src/types/saml-application.ts +++ b/packages/schemas/src/types/saml-application.ts @@ -2,6 +2,7 @@ import { type z } from 'zod'; import { Applications } from '../db-entries/application.js'; import { SamlApplicationConfigs } from '../db-entries/saml-application-config.js'; +import { SamlApplicationSecrets } from '../db-entries/saml-application-secret.js'; import { applicationCreateGuard, applicationPatchGuard } from './application.js'; @@ -37,6 +38,7 @@ export const samlApplicationPatchGuard = applicationPatchGuard export type PatchSamlApplication = z.infer; +// Make sure the `privateKey` is not exposed in the response. export const samlApplicationSecretResponseGuard = SamlApplicationSecrets.guard.omit({ tenantId: true, applicationId: true,