From d42efd6723680a52ad556c99d0be090e5b5b3ef3 Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Thu, 28 Dec 2023 12:38:59 -0500 Subject: [PATCH] Topics/k1ch/ introduce get/personas-permissions (#80) * feat: topics/k1ch/ introduce get/personas-permissions * chore: topics/k1ch/admin-get-persona-permissions/add tests * chore: topics/k1ch/admin-get-personas-permissions/minor fixes * chore: topics/k1ch/jsDoc for usherDb knex instance --- database/layer/admin-persona.js | 22 ++++++ database/layer/knex.js | 11 +++ database/test/db-admin-persona.test.js | 56 +++++++++++---- .../src/api_endpoints/personas/permissions.js | 18 +++++ server/src/api_endpoints/personas/utils.js | 15 ++++ ...ndpoint_admin_personas_permissions.test.js | 70 +++++++++++++++++++ server/the-usher-openapi-spec.yaml | 50 +++++++++++++ 7 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 server/src/api_endpoints/personas/permissions.js create mode 100644 server/src/api_endpoints/personas/utils.js create mode 100644 server/test/endpoint_admin_personas_permissions.test.js diff --git a/database/layer/admin-persona.js b/database/layer/admin-persona.js index da8aaaa..f871290 100644 --- a/database/layer/admin-persona.js +++ b/database/layer/admin-persona.js @@ -73,9 +73,31 @@ const insertPersonaByTenantKey = async (tenantKey, subClaim, userContext = '') = } } +const getPersona = async (personaKey) => { + try { + return await usherDb('personas').select('*').where({ key: personaKey }).first(); + } catch (err) { + throw pgErrorHandler(err) + } +} + +const getPersonaPermissions = async (personaKey) => { + try { + return await usherDb('permissions') + .select('permissions.key', 'permissions.name', 'permissions.description', 'permissions.clientkey') + .join('personapermissions', 'permissions.key', 'personapermissions.permissionkey') + .join('personas', 'personapermissions.personakey', 'personas.key') + .where('personas.key', personaKey) + } catch (err) { + throw pgErrorHandler(err) + } +} + module.exports = { insertPersona, deletePersona, updatePersona, insertPersonaByTenantKey, + getPersona, + getPersonaPermissions, } diff --git a/database/layer/knex.js b/database/layer/knex.js index 2c742b2..1ac1794 100644 --- a/database/layer/knex.js +++ b/database/layer/knex.js @@ -1,5 +1,16 @@ const knex = require('knex'); const knexDbConfig = require('../knexfile'); + +/** + * Usher DB connection instance. + * @name usherDb + * @type {import('knex')} + * @desc This instance provides a connection to the Usher database using Knex.js + * @example // To import usherDb instance + * const { usherDb } = require('./knex'); + * @example // To perform a database query + * const persona = await usherDb('personas').where('key', personaKey).first(); + */ const usherDb = knex(knexDbConfig); module.exports = { usherDb } diff --git a/database/test/db-admin-persona.test.js b/database/test/db-admin-persona.test.js index fd83344..ad28c69 100644 --- a/database/test/db-admin-persona.test.js +++ b/database/test/db-admin-persona.test.js @@ -1,29 +1,29 @@ const { describe, it } = require('mocha') const assert = require('assert') -const postPersonas = require('../layer/admin-persona.js') +const adminPersonas = require('../layer/admin-persona.js') const { usherDb } = require('../layer/knex') describe('Admin persona view', () => { describe('Test INSERT personas', () => { it('Should insert persona without an exception', async () => { - const insertResult = await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-1@dmgtoocto.com', '') + const insertResult = await adminPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-1@dmgtoocto.com', '') assert.strictEqual(insertResult, 'Insert successful') - await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-1@dmgtoocto.com', '') + await adminPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-1@dmgtoocto.com', '') }) it('Should fail to insert for a nonexistent tenant', async () => { - const insertResult = await postPersonas.insertPersona('test-tenant1 Non-existent', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-3@dmgtoocto.com', '') + const insertResult = await adminPersonas.insertPersona('test-tenant1 Non-existent', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-3@dmgtoocto.com', '') assert.strictEqual(insertResult, 'Insert failed: Tenant does not exist matching tenantname test-tenant1 Non-existent iss_claim http://idp.dmgt.com.mock.localhost:3002/') }) it('Should fail to insert duplicate tenant/persona combination - check tenantname', async () => { - await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-x@dmgtoocto.com', '') - const result = await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-x@dmgtoocto.com', '') + await adminPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-x@dmgtoocto.com', '') + const result = await adminPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-x@dmgtoocto.com', '') assert.strictEqual(result, 'Insert failed: A persona (sub_claim = test-dmgt-oocto-x@dmgtoocto.com; user_context = ) already exists on tenantname test-tenant1 iss_claim http://idp.dmgt.com.mock.localhost:3002/') - await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-x@dmgtoocto.com', '') + await adminPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-x@dmgtoocto.com', '') }) it('Should insert persona by tenant key without an exception', async () => { const subClaim = 'test-user@the-usher.com' const [tenant] = await usherDb('tenants').select('*').limit(1) - const persona = await postPersonas.insertPersonaByTenantKey(tenant.key, subClaim) + const persona = await adminPersonas.insertPersonaByTenantKey(tenant.key, subClaim) assert.strictEqual(persona.sub_claim, subClaim) await usherDb('personas').where({ key: persona.key }).del() }) @@ -31,25 +31,53 @@ describe('Admin persona view', () => { describe('Test UPDATE personas', () => { it('Should update persona without an exception by tenantname', async () => { - await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-5@dmgtoocto.com', '') - const resultTenantname = await postPersonas.updatePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-5@dmgtoocto.com', 'test-dmgt-oocto-7@dmgtoocto.com', '', '') + await adminPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-5@dmgtoocto.com', '') + const resultTenantname = await adminPersonas.updatePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-5@dmgtoocto.com', 'test-dmgt-oocto-7@dmgtoocto.com', '', '') assert.strictEqual(resultTenantname, 'Update successful') - await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-7@dmgtoocto.com', '') + await adminPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'test-dmgt-oocto-7@dmgtoocto.com', '') }) it('Should fail to update for a nonexistent tenant', async () => { - const resultTenantname = await postPersonas.updatePersona('test-tenant1 Non-existent', 'http://idp.dmgt.com.mock.localhost:3002/', 'auth0|test-persona2-REPLACE', 'should_not_replace_sub_claim', '', '') + const resultTenantname = await adminPersonas.updatePersona('test-tenant1 Non-existent', 'http://idp.dmgt.com.mock.localhost:3002/', 'auth0|test-persona2-REPLACE', 'should_not_replace_sub_claim', '', '') assert.strictEqual(resultTenantname, 'Update failed: A persona (sub_claim = auth0|test-persona2-REPLACE; user_context = ) does not exist on tenantname test-tenant1 Non-existent iss_claim http://idp.dmgt.com.mock.localhost:3002/') }) it('Should fail to update for a nonexistent persona', async () => { - const resultTenantname = await postPersonas.updatePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'does-not-exist@dmgtoocto.com', 'should_not_replace_sub_claim', '', '') + const resultTenantname = await adminPersonas.updatePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'does-not-exist@dmgtoocto.com', 'should_not_replace_sub_claim', '', '') assert.strictEqual(resultTenantname, 'Update failed: A persona (sub_claim = does-not-exist@dmgtoocto.com; user_context = ) does not exist on tenantname test-tenant1 iss_claim http://idp.dmgt.com.mock.localhost:3002/') }) }) describe('Test DELETE personas', () => { it('Should fail to delete a persona not linked to a tenant', async () => { - const resultDelete = await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'no-persona@dmgtoocto.com', '') + const resultDelete = await adminPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', 'no-persona@dmgtoocto.com', '') assert.strictEqual(resultDelete, 'Delete failed: A persona (sub_claim = no-persona@dmgtoocto.com; user_context = ) does not exist on tenantname test-tenant1 iss_claim http://idp.dmgt.com.mock.localhost:3002/') }) }) + + describe('Test GET personas', () => { + const invalidPersonaKey = 0; + it('Should return a valid persona', async () => { + const persona = await adminPersonas.getPersona(1) + assert.strictEqual(persona.key, 1) + }) + it('Should return undefined for invalid persona key', async () => { + const persona = await adminPersonas.getPersona(invalidPersonaKey) + assert.strictEqual(persona, undefined) + }) + }) + + describe('Test GET personas permissions', () => { + const invalidPersonaKey = 0; + it('Should return an array of permissions for the persona', async function () { + const { personakey } = await usherDb('personapermissions').select('*').first() || {} + if (!personakey) { + this.skip() + } + const personaPermissions = await adminPersonas.getPersonaPermissions(personakey) + assert.equal(!!personaPermissions.length, true) + }) + it('Should return an empty array', async () => { + const personaPermissions = await adminPersonas.getPersonaPermissions(invalidPersonaKey) + assert.equal(personaPermissions.length, 0) + }) + }) }) diff --git a/server/src/api_endpoints/personas/permissions.js b/server/src/api_endpoints/personas/permissions.js new file mode 100644 index 0000000..457a361 --- /dev/null +++ b/server/src/api_endpoints/personas/permissions.js @@ -0,0 +1,18 @@ +const createError = require('http-errors') +const dbAdminPersona = require('database/layer/admin-persona') +const { checkPersonaExists } = require('./utils') + +const getPersonaPermissions = async (req, res, next) => { + try { + const { persona_key: personaKey } = req.params + await checkPersonaExists(personaKey) + const permissions = await dbAdminPersona.getPersonaPermissions(personaKey) + res.status(200).send(permissions) + } catch ({ httpStatusCode = 500, message }) { + return next(createError(httpStatusCode, { message })) + } +} + +module.exports = { + getPersonaPermissions, +} diff --git a/server/src/api_endpoints/personas/utils.js b/server/src/api_endpoints/personas/utils.js new file mode 100644 index 0000000..760f530 --- /dev/null +++ b/server/src/api_endpoints/personas/utils.js @@ -0,0 +1,15 @@ +const dbAdminPersona = require('database/layer/admin-persona') + +const checkPersonaExists = async (personaKey) => { + const persona = await dbAdminPersona.getPersona(personaKey) + if (!persona) { + throw { + httpStatusCode: 404, + message: 'Persona does not exist!' + } + } +} + +module.exports = { + checkPersonaExists, +} diff --git a/server/test/endpoint_admin_personas_permissions.test.js b/server/test/endpoint_admin_personas_permissions.test.js new file mode 100644 index 0000000..11f274e --- /dev/null +++ b/server/test/endpoint_admin_personas_permissions.test.js @@ -0,0 +1,70 @@ +const { describe, it, before } = require('mocha') +const fetch = require('node-fetch') +const assert = require('assert') + +const { getAdmin1IdPToken, getTestUser1IdPToken } = require('./lib/tokens') +const { getServerUrl } = require('./lib/urls') +const { usherDb } = require('../../database/layer/knex') + + +describe('Admin Personas Permissions', () => { + let requestHeaders + const url = `${getServerUrl()}` + + before(async () => { + const adminAccessToken = await getAdmin1IdPToken() + requestHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${adminAccessToken}`, + } + }) + + describe('GET:/personas/{persona_key}/permissions', () => { + const invalidPersona = 0; + const validPersonaWithNoPermissions = 1 + + it('should return 200 and a list of permissions for the persona', async function () { + const { personakey } = await usherDb('personapermissions').select('*').first() || {} + if (!personakey) { + this.skip() + } + const response = await fetch(`${url}/personas/${personakey}/permissions`, { + method: 'GET', + headers: requestHeaders, + }) + assert.equal(response.status, 200) + const personaPermissions = await response.json() + assert.equal(personaPermissions.length > 0, true) + }) + + it('should return 200 and an empty array', async () => { + const response = await fetch(`${url}/personas/${validPersonaWithNoPermissions}/permissions`, { + method: 'GET', + headers: requestHeaders, + }) + assert.equal(response.status, 200) + const personaPermissions = await response.json() + assert.equal(personaPermissions.length, 0) + }) + + it('should return 404 and fail to get permissions for an invalid persona', async () => { + const response = await fetch(`${url}/personas/${invalidPersona}/permissions`, { + method: 'GET', + headers: requestHeaders, + }) + assert.equal(response.status, 404) + }) + + it('should return 401 due to lack of proper token', async () => { + const userAccessToken = await getTestUser1IdPToken() + const response = await fetch(`${url}/personas/${validPersonaWithNoPermissions}/permissions`, { + method: 'GET', + headers: { + ...requestHeaders, + Authorization: `Bearer ${userAccessToken}` + }, + }) + assert.equal(response.status, 401) + }) + }) +}) diff --git a/server/the-usher-openapi-spec.yaml b/server/the-usher-openapi-spec.yaml index 14f947b..9c63698 100644 --- a/server/the-usher-openapi-spec.yaml +++ b/server/the-usher-openapi-spec.yaml @@ -472,6 +472,29 @@ paths: 500: $ref: '#/components/responses/InternalError' + /personas/{persona_key}/permissions: + get: + 'x-swagger-router-controller': 'personas/permissions' + operationId: getPersonaPermissions + parameters: + - $ref: '#/components/parameters/personaKeyPathParam' + tags: + - Admin APIs + security: + - bearerAdminAuth: [] + responses: + 200: + description: Returns a list of permissions for the subject persona + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PermissionObject" + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalError' /clients/{client_id}: get: @@ -635,6 +658,13 @@ components: required: true schema: type: integer + personaKeyPathParam: + name: persona_key + description: The unique persona identifier + in: path + required: true + schema: + type: integer # user_context userContextParam: name: user_context @@ -739,6 +769,26 @@ components: $ref: '#/components/schemas/EntityDescriptionDef' required: - permission + + PermissionObject: + type: object + properties: + key: + type: integer + minimum: 1 + format: int32 + clientkey: + type: integer + minimum: 1 + format: int32 + name: + $ref: '#/components/schemas/EntityNameDef' + description: + $ref: '#/components/schemas/EntityDescriptionDef' + required: + - key + - name + - clientkey #--------------------- ArrayOfPermissions: type: array