From 6f13db9add08513545fb4fa0e5a51ffc124033e1 Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Sat, 9 Nov 2024 19:18:29 -0500 Subject: [PATCH] Topic /k1ch/ introduce API `PUT:/roles/{role_key}/permissions` (#132) * feat: k1ch / introduce PUT:/roles/{role_key}/permissions * chore: k1ch/ add tests to admin-rolepermissions db layer * chore: k1ch / add tests for PUT:/roles/{role_key}/permissions * bug-fix: minor --- database/layer/admin-rolepermissions.js | 55 ++++++++++- .../test/db-admin-rolepermissions.test.js | 93 +++++++++++++++++++ server/src/api_endpoints/roles/permissions.js | 29 +++++- server/src/api_endpoints/roles/utils.js | 19 ++++ .../endpoint_admin_roles_permissions.test.js | 86 +++++++++++++++++ server/the-usher-openapi-spec.yaml | 40 +++++++- 6 files changed, 318 insertions(+), 4 deletions(-) diff --git a/database/layer/admin-rolepermissions.js b/database/layer/admin-rolepermissions.js index 5a798bf..6383f5e 100644 --- a/database/layer/admin-rolepermissions.js +++ b/database/layer/admin-rolepermissions.js @@ -70,8 +70,61 @@ const getRolePermissions = async (roleKey) => { } } +/** + * Retrieves permissions for the role within the same client + * + * @param {number} roleKey - The key of the role + * @param {Array} permissionKeys - An array of permission keys + * @returns {Promise} - A promise that resolves with the retrieved permissions + * @throws {Error} - If there's an error during the database operation. + */ +const getPermissionsForRoleWithinSameClient = async (roleKey, permissionKeys) => { + try { + const permissions = await usherDb('permissions as p') + .select('p.*') + .whereIn('p.key', permissionKeys) + .andWhere('p.clientkey', '=', usherDb('roles as r') + .select('r.clientkey') + .where('r.key', roleKey) + .first() + ) + return permissions + } catch (error) { + throw pgErrorHandler(error) + } +} + +/** + * Insert multiple records for role permissions and ignore conflicts + * This means if several permissions are inserted and some of them already exist, + * the existing records will **not** be returned in the Promise results + * + * @param {number} roleKey - The role key + * @param {number[]} permissionKeys - An array of permission keys + * @returns {Promise} - A promise that resolves to an array of inserted rolepermissions records + */ +const insertRolePermissions = async (roleKey, permissionKeys) => { + try { + const rolePermissions = permissionKeys.map((permissionKey) => { + return { + rolekey: roleKey, + permissionkey: permissionKey, + } + }) + return await usherDb('rolepermissions') + .insert(rolePermissions) + .onConflict(['rolekey', 'permissionkey']) + .ignore() + .returning('*') + } catch (err) { + throw pgErrorHandler(err) + } +} + module.exports = { insertRolePermissionByClientId, deleteRolePermissionByClientId, - getRolePermissions + getRolePermissions, + getPermissionsForRoleWithinSameClient, + insertRolePermissions, } diff --git a/database/test/db-admin-rolepermissions.test.js b/database/test/db-admin-rolepermissions.test.js index bee5097..f990e31 100644 --- a/database/test/db-admin-rolepermissions.test.js +++ b/database/test/db-admin-rolepermissions.test.js @@ -20,4 +20,97 @@ describe('Admin role permissions view', () => { assert.equal(rolePermissions.length, 0) }) }) + + describe('Test insertRolePermissions', () => { + let validRoleKey + let validPermissionKey + const invalidRoleKey = 0 + const invalidPermissionKey = 0 + + before(async () => { + const { key: roleKey, clientkey: clientKey } = await usherDb('roles').select('key', 'clientkey').first() + validRoleKey = roleKey + const { key: permissionKey } = await usherDb('permissions').select('key').where({ clientkey: clientKey }).first() + validPermissionKey = permissionKey + await usherDb('rolepermissions').where({ rolekey: validRoleKey }).del() + }) + + it('Should return an array of inserted rolepermissions records', async () => { + const rolePermissions = await adminRolePermissions.insertRolePermissions(validRoleKey, [validPermissionKey]) + assert.equal(rolePermissions.length, 1) + assert.equal(rolePermissions[0].rolekey, validRoleKey) + assert.equal(rolePermissions[0].permissionkey, validPermissionKey) + }) + + it('Should ignore the duplicate permission keys', async () => { + const rolePermissions = await adminRolePermissions.insertRolePermissions(validRoleKey, [validPermissionKey, validPermissionKey]) + assert.equal(rolePermissions.length, 1) + }) + + it('Should handle multiple permission key inserts', async () => { + const rolePermissions1 = await adminRolePermissions.insertRolePermissions(validRoleKey, [validPermissionKey]) + const rolePermissions2 = await adminRolePermissions.insertRolePermissions(validRoleKey, [validPermissionKey]) + assert.equal(rolePermissions1.length, 1) + assert.equal(rolePermissions2.length, 0) + }) + + it('Should fail due to invalid role key', async () => { + try { + await adminRolePermissions.insertRolePermissions(invalidRoleKey, [validPermissionKey]) + assert.fail('Should fail to insertRolePermissions!') + } catch (err) { + assert.ok(err instanceof Error) + } + }) + + it('Should fail due to invalid permission key', async () => { + try { + await adminRolePermissions.insertRolePermissions(validRoleKey, [invalidPermissionKey]) + assert.fail('Should fail to insertRolePermissions!') + } catch (err) { + assert.ok(err instanceof Error) + } + }) + + afterEach(async () => { + await usherDb('rolepermissions').where({ rolekey: validRoleKey }).del() + }) + }) + + describe('Test getPermissionsForRoleWithinSameClient', () => { + let validRoleKey + let validPermissionKeys + let invalidPermissionKey + const invalidRoleKey = 0 + + before(async () => { + const { key: roleKey, clientkey: clientKey } = await usherDb('roles').select('key', 'clientkey').first() + validRoleKey = roleKey + + const permissions = await usherDb('permissions').select('key').where({ clientkey: clientKey }).limit(2) + validPermissionKeys = permissions.map((p) => p.key) + + invalidPermissionKey = (await usherDb('permissions') + .select('key') + .whereNot({ clientkey: clientKey }) + .first()).key + }) + + it('Should return permissions for the given role within the same client', async () => { + const permissions = await adminRolePermissions.getPermissionsForRoleWithinSameClient(validRoleKey, validPermissionKeys) + assert.equal(permissions.length, validPermissionKeys.length) + assert.ok(permissions.every(({ key }) => validPermissionKeys.includes(key))) + }) + + it('Should not include a permission that is not valid', async () => { + const permissions = await adminRolePermissions.getPermissionsForRoleWithinSameClient(validRoleKey, [...validPermissionKeys, invalidPermissionKey]) + assert.equal(permissions.length, validPermissionKeys.length) + assert.ok(permissions.every(({ key }) => validPermissionKeys.includes(key))) + }) + + it('Should return an empty array when permissions are invalid', async () => { + const permissions = await adminRolePermissions.getPermissionsForRoleWithinSameClient(invalidRoleKey, [invalidPermissionKey]) + assert.equal(permissions.length, 0) + }) + }) }) diff --git a/server/src/api_endpoints/roles/permissions.js b/server/src/api_endpoints/roles/permissions.js index b076006..857f2c7 100644 --- a/server/src/api_endpoints/roles/permissions.js +++ b/server/src/api_endpoints/roles/permissions.js @@ -1,6 +1,6 @@ const createError = require('http-errors') const dbAdminRolePermissions = require('database/layer/admin-rolepermissions') -const { checkRoleExists } = require('./utils') +const { checkRoleExists, checkRolePermissionsValidity } = require('./utils') const getRolesPermissions = async (req, res, next) => { try { @@ -13,6 +13,33 @@ const getRolesPermissions = async (req, res, next) => { } } +/** + * HTTP Request Handler for assigning a list of permissions to a role + * + * This function handles PUT requests to assign permissions to a specific role within the same client + * It ensures that the role exists, validates the permissions, and then updates the database accordingly + * + * @param {Object} req - The request object, containing parameters and body data + * @param {Object} res - The response object used to send a 204 status code with no content + * @param {Function} next - The next middleware function in the stack + * @returns {Promise} - A Promise that resolves when the role permissions are successfully created or an error occurs + */ +const createRolePermissions = async (req, res, next) => { + try { + const { role_key: roleKey } = req.params + await checkRoleExists(roleKey) + const permissionKeys = [...new Set(req.body)] + await checkRolePermissionsValidity(roleKey, permissionKeys) + await dbAdminRolePermissions.insertRolePermissions(roleKey, permissionKeys) + const locationUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}` + res.set('Location', locationUrl) + res.status(204).send() + } catch ({ httpStatusCode = 500, message }) { + return next(createError(httpStatusCode, { message })) + } +} + module.exports = { getRolesPermissions, + createRolePermissions, } diff --git a/server/src/api_endpoints/roles/utils.js b/server/src/api_endpoints/roles/utils.js index b5e0499..3adb585 100644 --- a/server/src/api_endpoints/roles/utils.js +++ b/server/src/api_endpoints/roles/utils.js @@ -1,4 +1,5 @@ const dbAdminRole = require('database/layer/admin-role') +const dbAdminRolePermissions = require('database/layer/admin-rolepermissions') const checkRoleExists = async (roleKey) => { const role = await dbAdminRole.getRole(roleKey) @@ -9,6 +10,24 @@ const checkRoleExists = async (roleKey) => { } } +/** + * Checks if provided permission keys are valid for the given role key + * Throws an error if any of the permissions are invalid + * + * @param {number} roleKey - The key of the role + * @param {number[]} permissionKeys - An array of permission keys to check for validity + * @throws {object} Error object with httpStatusCode and message properties + */ +const checkRolePermissionsValidity = async (roleKey, permissionKeys) => { + const validPermissions = await dbAdminRolePermissions.getPermissionsForRoleWithinSameClient(roleKey, permissionKeys) + if (validPermissions.length !== permissionKeys.length) { + const error = new Error('Permissions should be assigned to the same client as the subject role.') + error.httpStatusCode = 400 + throw error + } +} + module.exports = { checkRoleExists, + checkRolePermissionsValidity, } diff --git a/server/test/endpoint_admin_roles_permissions.test.js b/server/test/endpoint_admin_roles_permissions.test.js index b736e03..d54f2ad 100644 --- a/server/test/endpoint_admin_roles_permissions.test.js +++ b/server/test/endpoint_admin_roles_permissions.test.js @@ -96,4 +96,90 @@ describe('Admin Roles Permissions', () => { assert.equal(response.status, 404) }) }) + + describe('PUT:/roles/{role_key}/permissions', () => { + let validRoleKey + let validPermissionKeys + let invalidPermissionKey + const invalidRoleKey = 0 + + const putRolesPermissions = async (requestPayload, header = requestHeaders, roleKey = validRoleKey) => { + return await fetch(`${url}/roles/${roleKey}/permissions`, { + method: 'PUT', + headers: header, + body: JSON.stringify(requestPayload) + }) + } + + before(async () => { + const { key: roleKey, clientkey: clientKey } = await usherDb('roles').select('key', 'clientkey').first() + validRoleKey = roleKey + + const permissions = await usherDb('permissions').select('key').where({ clientkey: clientKey }).limit(2) + validPermissionKeys = permissions.map((p) => p.key) + + invalidPermissionKey = (await usherDb('permissions') + .select('key') + .whereNot({ clientkey: clientKey }) + .first()).key + }) + + it('should return 204, empty response body, and Location header to get all the role permissions', async () => { + const response = await putRolesPermissions(validPermissionKeys) + assert.equal(response.status, 204) + assert.equal(response.headers.get('Location'), response.url) + const responseBody = await response.text() + assert.equal(responseBody, '') + }) + + it('should return 204, should be able to handle duplicate keys in the body', async () => { + const response = await putRolesPermissions([...validPermissionKeys, ...validPermissionKeys]) + assert.equal(response.status, 204) + }) + + it('should return 204, ignore to create role permissions that already exist', async () => { + await putRolesPermissions(validPermissionKeys) + const response = await putRolesPermissions(validPermissionKeys) + assert.equal(response.status, 204) + }) + + it('should return 400, a permission does not belong to the same client as the role', async () => { + const response = await putRolesPermissions([...validPermissionKeys, invalidPermissionKey]) + assert.equal(response.status, 400) + }) + + it('should return 400, for three different invalid request payloads', async () => { + const [emptyBodyResponse, invalidBodyResponse, invalidPermissionResponse] = await Promise.all( + [ + putRolesPermissions(), + putRolesPermissions({}), + putRolesPermissions([invalidPermissionKey]), + ] + ) + assert.ok([ + emptyBodyResponse.status, + invalidBodyResponse.status, + invalidPermissionResponse.status].every((status) => status === 400)) + }) + + it('should return 401, unauthorized token', async () => { + const userAccessToken = await getTestUser1IdPToken() + const response = await putRolesPermissions( + validPermissionKeys, + { + ...requestHeaders, + Authorization: `Bearer ${userAccessToken}` + }) + assert.equal(response.status, 401) + }) + + it('should return 404, fail to create role permissions for an invalid role', async () => { + const response = await putRolesPermissions(validPermissionKeys, requestHeaders, invalidRoleKey) + assert.equal(response.status, 404) + }) + + afterEach(async () => { + await usherDb('rolepermissions').where({ rolekey: validRoleKey }).del() + }) + }) }) diff --git a/server/the-usher-openapi-spec.yaml b/server/the-usher-openapi-spec.yaml index 605b021..c5090fc 100644 --- a/server/the-usher-openapi-spec.yaml +++ b/server/the-usher-openapi-spec.yaml @@ -432,12 +432,12 @@ paths: $ref: '#/components/responses/NotFound' /roles/{role_key}/permissions: + parameters: + - $ref: '#/components/parameters/roleKeyPathParam' get: 'x-swagger-router-controller': 'roles/permissions' operationId: getRolesPermissions summary: "Roles: Get list of Permissions" - parameters: - - $ref: '#/components/parameters/roleKeyPathParam' tags: - Admin APIs security: @@ -459,6 +459,42 @@ paths: $ref: '#/components/responses/InternalError' 503: $ref: '#/components/responses/ServiceUnavailableError' + put: + 'x-swagger-router-controller': 'roles/permissions' + operationId: createRolePermissions + summary: Assigns permissions to the subject role + tags: + - Admin APIs + security: + - bearerAdminAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + minItems: 1 + items: + type: integer + minimum: 1 + responses: + 204: + description: Successfully created permissions for the subject role + headers: + Location: + description: The URL to get all the role permissions + schema: + type: string + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/ServiceUnavailableError' /personas: get: