diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js new file mode 100644 index 00000000..0cddadf6 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js @@ -0,0 +1,43 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProvider = await SamlAuthProvider.query().insert( + samlAuthProviderParams(request) + ); + + renderObject(response, samlAuthProvider, { + serializer: 'AdminSamlAuthProvider', + status: 201, + }); +}; + +const samlAuthProviderParams = (request) => { + const { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + defaultRoleId, + active, + } = request.body; + + return { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + defaultRoleId, + active, + }; +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.test.js new file mode 100644 index 00000000..517b59d6 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.test.js @@ -0,0 +1,78 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import createSamlAuthProviderMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('POST /api/v1/admin/saml-auth-provider', () => { + let currentUser, token, role; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the created saml auth provider', async () => { + const samlAuthProviderPayload = { + active: true, + name: 'Name', + issuer: 'theclientid', + certificate: 'dummycert', + entryPoint: 'http://localhost:8080/realms/automatisch/protocol/saml', + signatureAlgorithm: 'sha256', + defaultRoleId: role.id, + firstnameAttributeName: 'urn:oid:2.5.4.42', + surnameAttributeName: 'urn:oid:2.5.4.4', + emailAttributeName: 'urn:oid:1.2.840.113549.1.9.1', + roleAttributeName: 'Role', + }; + + const response = await request(app) + .post('/api/v1/admin/saml-auth-providers') + .set('Authorization', token) + .send(samlAuthProviderPayload) + .expect(201); + + const expectedPayload = await createSamlAuthProviderMock({ + id: response.body.data.id, + ...samlAuthProviderPayload, + }); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return unprocessable entity response for invalid data', async () => { + const response = await request(app) + .post('/api/v1/admin/saml-auth-providers') + .set('Authorization', token) + .send({ + active: true, + name: 'Name', + issuer: 'theclientid', + signatureAlgorithm: 'invalid', + firstnameAttributeName: 'urn:oid:2.5.4.42', + surnameAttributeName: 'urn:oid:2.5.4.4', + emailAttributeName: 'urn:oid:1.2.840.113549.1.9.1', + roleAttributeName: 123, + }) + .expect(422); + + expect(response.body).toStrictEqual({ + errors: { + certificate: ["must have required property 'certificate'"], + entryPoint: ["must have required property 'entryPoint'"], + defaultRoleId: ["must have required property 'defaultRoleId'"], + signatureAlgorithm: ['must be equal to one of the allowed values'], + roleAttributeName: ['must be string'], + }, + meta: { type: 'ModelValidation' }, + }); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.js new file mode 100644 index 00000000..bf678e95 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.js @@ -0,0 +1,45 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProvider = await SamlAuthProvider.query() + .patchAndFetchById( + request.params.samlAuthProviderId, + samlAuthProviderParams(request) + ) + .throwIfNotFound(); + + renderObject(response, samlAuthProvider, { + serializer: 'AdminSamlAuthProvider', + }); +}; + +const samlAuthProviderParams = (request) => { + const { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + defaultRoleId, + active, + } = request.body; + + return { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + defaultRoleId, + active, + }; +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.test.js new file mode 100644 index 00000000..f8c858f1 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.test.js @@ -0,0 +1,119 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; +import createSamlAuthProviderMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('PATCH /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => { + let currentUser, token, role; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the updated saml auth provider', async () => { + const samlAuthProviderPayload = { + active: true, + name: 'Name', + issuer: 'theclientid', + certificate: 'dummycert', + entryPoint: 'http://localhost:8080/realms/automatisch/protocol/saml', + signatureAlgorithm: 'sha256', + defaultRoleId: role.id, + firstnameAttributeName: 'urn:oid:2.5.4.42', + surnameAttributeName: 'urn:oid:2.5.4.4', + emailAttributeName: 'urn:oid:1.2.840.113549.1.9.1', + roleAttributeName: 'Role', + }; + + const samlAuthProvider = await createSamlAuthProvider( + samlAuthProviderPayload + ); + + const response = await request(app) + .patch(`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}`) + .set('Authorization', token) + .send({ + active: false, + name: 'Archived', + }) + .expect(200); + + const refetchedSamlAuthProvider = await samlAuthProvider.$query(); + + const expectedPayload = await createSamlAuthProviderMock({ + ...refetchedSamlAuthProvider, + name: 'Archived', + active: false, + }); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return unprocessable entity response for invalid data', async () => { + const samlAuthProviderPayload = { + active: true, + name: 'Name', + issuer: 'theclientid', + certificate: 'dummycert', + entryPoint: 'http://localhost:8080/realms/automatisch/protocol/saml', + signatureAlgorithm: 'sha256', + defaultRoleId: role.id, + firstnameAttributeName: 'urn:oid:2.5.4.42', + surnameAttributeName: 'urn:oid:2.5.4.4', + emailAttributeName: 'urn:oid:1.2.840.113549.1.9.1', + roleAttributeName: 'Role', + }; + + const samlAuthProvider = await createSamlAuthProvider( + samlAuthProviderPayload + ); + + const response = await request(app) + .patch(`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}`) + .set('Authorization', token) + .send({ + active: 'true', + name: 123, + roleAttributeName: 123, + }) + .expect(422); + + expect(response.body).toStrictEqual({ + errors: { + name: ['must be string'], + active: ['must be boolean'], + roleAttributeName: ['must be string'], + }, + meta: { type: 'ModelValidation' }, + }); + }); + + it('should return not found response for not existing SAML auth provider UUID', async () => { + const notExistingSamlAuthProviderUUID = Crypto.randomUUID(); + + await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${notExistingSamlAuthProviderUUID}` + ) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await request(app) + .patch('/api/v1/admin/saml-auth-providers/invalidSamlAuthProviderUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js b/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js index 62f05859..57e333f8 100644 --- a/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js +++ b/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js @@ -2,6 +2,8 @@ import { Router } from 'express'; import { authenticateUser } from '../../../../helpers/authentication.js'; import { authorizeAdmin } from '../../../../helpers/authorization.js'; import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import createSamlAuthProviderAction from '../../../../controllers/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js'; +import updateSamlAuthProviderAction from '../../../../controllers/api/v1/admin/saml-auth-providers/update-saml-auth-provider.ee.js'; import getSamlAuthProvidersAction from '../../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js'; import getSamlAuthProviderAction from '../../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js'; import getRoleMappingsAction from '../../../../controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js'; @@ -16,6 +18,14 @@ router.get( getSamlAuthProvidersAction ); +router.post( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + createSamlAuthProviderAction +); + router.get( '/:samlAuthProviderId', authenticateUser, @@ -32,4 +42,12 @@ router.get( getRoleMappingsAction ); +router.patch( + '/:samlAuthProviderId', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + updateSamlAuthProviderAction +); + export default router; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js new file mode 100644 index 00000000..2a2a7333 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/create-saml-auth-provider.ee.js @@ -0,0 +1,29 @@ +const createSamlAuthProviderMock = async (samlAuthProvider) => { + const data = { + active: samlAuthProvider.active, + certificate: samlAuthProvider.certificate, + defaultRoleId: samlAuthProvider.defaultRoleId, + emailAttributeName: samlAuthProvider.emailAttributeName, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + id: samlAuthProvider.id, + issuer: samlAuthProvider.issuer, + name: samlAuthProvider.name, + roleAttributeName: samlAuthProvider.roleAttributeName, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'SamlAuthProvider', + }, + }; +}; + +export default createSamlAuthProviderMock;