From 062199d0e3087eead27bc3c6d1f4c6a10095feab Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 2 Sep 2024 15:29:16 +0000 Subject: [PATCH] feat: write endpoints to update SamlAuthProvidersRoleMappings --- .../update-role-mappings.ee.js | 50 +++++ .../update-role-mappings.ee.test.js | 178 ++++++++++++++++++ packages/backend/src/helpers/error-handler.js | 13 +- .../api/v1/admin/saml-auth-providers.ee.js | 9 + .../saml-auth-providers-role-mapping.js | 16 ++ .../update-role-mappings.ee.js | 23 +++ 6 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js create mode 100644 packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js create mode 100644 packages/backend/test/factories/saml-auth-providers-role-mapping.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js new file mode 100644 index 00000000..0aa122dc --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js @@ -0,0 +1,50 @@ +import isEmpty from 'lodash/isEmpty.js'; +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; +import SamlAuthProvidersRoleMapping from '../../../../../models/saml-auth-providers-role-mapping.ee.js'; + +export default async (request, response) => { + const samlAuthProviderId = request.params.samlAuthProviderId; + + const samlAuthProvider = await SamlAuthProvider.query() + .findById(samlAuthProviderId) + .throwIfNotFound(); + + const samlAuthProvidersRoleMappings = + await SamlAuthProvidersRoleMapping.transaction(async (trx) => { + await samlAuthProvider + .$relatedQuery('samlAuthProvidersRoleMappings', trx) + .delete(); + + const roleMappings = samlAuthProvidersRoleMappingsParams(request); + + if (isEmpty(roleMappings)) { + return []; + } + + const samlAuthProvidersRoleMappingsData = roleMappings.map( + (samlAuthProvidersRoleMapping) => ({ + ...samlAuthProvidersRoleMapping, + samlAuthProviderId: samlAuthProvider.id, + }) + ); + + const samlAuthProvidersRoleMappings = + await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch( + samlAuthProvidersRoleMappingsData + ); + + return samlAuthProvidersRoleMappings; + }); + + renderObject(response, samlAuthProvidersRoleMappings); +}; + +const samlAuthProvidersRoleMappingsParams = (request) => { + const roleMappings = request.body; + + return roleMappings.map(({ roleId, remoteRoleName }) => ({ + roleId, + remoteRoleName, + })); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js new file mode 100644 index 00000000..b4e6ab3a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js @@ -0,0 +1,178 @@ +import Crypto from 'node:crypto'; +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 { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; +import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js'; +import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappings', () => { + let samlAuthProvider, currentUser, userRole, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + userRole = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: userRole.id }); + + samlAuthProvider = await createSamlAuthProvider(); + await createSamlAuthProvidersRoleMapping({ + samlAuthProviderId: samlAuthProvider.id, + }); + await createSamlAuthProvidersRoleMapping({ + samlAuthProviderId: samlAuthProvider.id, + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should update role mappings', async () => { + const roleMappings = [ + { + roleId: userRole.id, + remoteRoleName: 'Admin', + }, + ]; + + const response = await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` + ) + .set('Authorization', token) + .send(roleMappings) + .expect(200); + + const expectedPayload = await createRoleMappingsMock([ + { + roleId: userRole.id, + remoteRoleName: 'Admin', + id: response.body.data[0].id, + samlAuthProviderId: samlAuthProvider.id, + }, + ]); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should delete role mappings when given empty role mappings', async () => { + const existingRoleMappings = await samlAuthProvider.$relatedQuery( + 'samlAuthProvidersRoleMappings' + ); + + expect(existingRoleMappings.length).toBe(2); + + const response = await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` + ) + .set('Authorization', token) + .send([]) + .expect(200); + + const expectedPayload = await createRoleMappingsMock([]); + + expect(response.body).toStrictEqual({ + ...expectedPayload, + meta: { + ...expectedPayload.meta, + type: 'Object', + }, + }); + }); + + it('should return unprocessable entity response for not existing role UUID', async () => { + const notExistingRoleUUID = Crypto.randomUUID(); + const roleMappings = [ + { + roleId: notExistingRoleUUID, + remoteRoleName: 'Admin', + }, + ]; + + await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` + ) + .set('Authorization', token) + .send(roleMappings) + .expect(500); + }); + + it('should return unprocessable entity response for invalid data', async () => { + const roleMappings = [ + { + roleId: userRole.id, + remoteRoleName: {}, + }, + ]; + + const response = await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` + ) + .set('Authorization', token) + .send(roleMappings) + .expect(422); + + expect(response.body).toStrictEqual({ + errors: { + remoteRoleName: ['must be string'], + }, + meta: { + type: 'ModelValidation', + }, + }); + }); + + it('should return not found response for not existing SAML auth provider UUID', async () => { + const notExistingSamlAuthProviderUUID = Crypto.randomUUID(); + const roleMappings = [ + { + roleId: userRole.id, + remoteRoleName: 'Admin', + }, + ]; + + await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${notExistingSamlAuthProviderUUID}/role-mappings` + ) + .set('Authorization', token) + .send(roleMappings) + .expect(404); + }); + + it('should not delete existing role mapping when error thrown', async () => { + const roleMappings = [ + { + roleId: userRole.id, + remoteRoleName: { + invalid: 'data', + }, + }, + ]; + + const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery( + 'samlAuthProvidersRoleMappings' + ); + + await request(app) + .patch( + `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` + ) + .set('Authorization', token) + .send(roleMappings) + .expect(422); + + const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery( + 'samlAuthProvidersRoleMappings' + ); + + expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest); + expect(roleMappingsAfterRequest.length).toBe(2); + }); +}); diff --git a/packages/backend/src/helpers/error-handler.js b/packages/backend/src/helpers/error-handler.js index ecfa9c18..63c80bbb 100644 --- a/packages/backend/src/helpers/error-handler.js +++ b/packages/backend/src/helpers/error-handler.js @@ -1,8 +1,13 @@ import logger from './logger.js'; import objection from 'objection'; import * as Sentry from './sentry.ee.js'; -const { NotFoundError, DataError, ValidationError, UniqueViolationError } = - objection; +const { + NotFoundError, + DataError, + ForeignKeyViolationError, + ValidationError, + UniqueViolationError, +} = objection; import NotAuthorizedError from '../errors/not-authorized.js'; import HttpError from '../errors/http.js'; import { @@ -29,6 +34,10 @@ const errorHandler = (error, request, response, next) => { renderUniqueViolationError(response, error); } + if (error instanceof ForeignKeyViolationError) { + response.status(500).end(); + } + if (error instanceof DataError) { response.status(400).end(); } 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 57e333f8..eb243b82 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 @@ -7,6 +7,7 @@ import updateSamlAuthProviderAction from '../../../../controllers/api/v1/admin/s 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'; +import updateRoleMappingsAction from '../../../../controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js'; const router = Router(); @@ -50,4 +51,12 @@ router.patch( updateSamlAuthProviderAction ); +router.patch( + '/:samlAuthProviderId/role-mappings', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + updateRoleMappingsAction +); + export default router; diff --git a/packages/backend/test/factories/saml-auth-providers-role-mapping.js b/packages/backend/test/factories/saml-auth-providers-role-mapping.js new file mode 100644 index 00000000..72b23c06 --- /dev/null +++ b/packages/backend/test/factories/saml-auth-providers-role-mapping.js @@ -0,0 +1,16 @@ +import { faker } from '@faker-js/faker'; +import { createRole } from './role.js'; +import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js'; +import { createSamlAuthProvider } from './saml-auth-provider.ee.js'; + +export const createSamlAuthProvidersRoleMapping = async (params = {}) => { + params.roleId = params.roleId || (await createRole()).id; + params.samlAuthProviderId = + params.samlAuthProviderId || (await createSamlAuthProvider()).id; + params.remoteRoleName = params.remoteRoleName || faker.person.jobType(); + + const samlAuthProvider = + await SamlAuthProvidersRoleMapping.query().insertAndFetch(params); + + return samlAuthProvider; +}; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js new file mode 100644 index 00000000..2ff8ca16 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js @@ -0,0 +1,23 @@ +const createRoleMappingsMock = async (roleMappings) => { + const data = roleMappings.map((roleMapping) => { + return { + id: roleMapping.id, + samlAuthProviderId: roleMapping.samlAuthProviderId, + roleId: roleMapping.roleId, + remoteRoleName: roleMapping.remoteRoleName, + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'SamlAuthProvidersRoleMapping', + }, + }; +}; + +export default createRoleMappingsMock;