From 34755fc33bfdb0cb4f8848dd673d1c3ebc07415e Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Mon, 23 Dec 2024 12:33:08 -0500 Subject: [PATCH] Topic / k1ch / Introduce `GET:/permissions` and `GET:/clients/{client_id}/permissions` APIs (#135) * feat: topic/k1ch/introduce API - GET/permissions * feat: topic/k1ch/ introduce API / GET/clients/{id}/permissions * chore: db-admin-permissions / add tests for getPermissions * chore: k1ch / introduce test for GET:/permissions * chore: k1ch / add test for GET:/clients/{client_id}/permissions * chore: minor changes * chore: k1ch / fix test GH workflow --- .github/workflows/run-tests.yaml | 1 + database/layer/admin-permission.js | 34 ++++++++ database/test/db-admin-permissions.test.js | 43 ++++++++++ .../src/api_endpoints/clients/permissions.js | 21 +++++ .../src/api_endpoints/endpoint_permissions.js | 23 +++++ server/test/endpoint_clients.test.js | 39 +++++++++ server/test/endpoint_permissions.test.js | 85 +++++++++++++++++++ server/the-usher-openapi-spec.yaml | 85 ++++++++++++++++++- 8 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 server/src/api_endpoints/endpoint_permissions.js create mode 100644 server/test/endpoint_permissions.test.js diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index f8d3520..98ebbb9 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -17,6 +17,7 @@ jobs: PGUSER: postgres PGPASSWORD: tehsecure PGSCHEMA: usher + KNEX_POOL_REAP_INTERVAL_MILLIS: 100000 steps: - run: sudo ethtool -K eth0 tx off rx off - uses: actions/checkout@v4 diff --git a/database/layer/admin-permission.js b/database/layer/admin-permission.js index ef910b6..239b17f 100644 --- a/database/layer/admin-permission.js +++ b/database/layer/admin-permission.js @@ -114,6 +114,39 @@ const getPermissionsByNameClientKey = async (name, clientKey) => { } } +/** + * Get permissions by optional filters + * + * @param {Object} filters - The filters to apply + * @param {string} [filters.name] - The name of the permission + * @param {string} [filters.clientId] - The client id + * @param {number} [filters.clientKey] - The client key + * @returns {Promise>} - A promise that resolves to an array of permissions + */ +const getPermissions = async (filters = {}) => { + try { + const query = usherDb('permissions') + .join('clients', 'permissions.clientkey', '=', 'clients.key') + .select('permissions.*', 'clients.client_id') + + const { clientId, name, clientKey } = filters + if (clientId) { + query.where('clients.client_id', 'ilike', `%${clientId}%`) + } + if (name) { + query.where('permissions.name', 'ilike', `%${name}%`) + } + if (clientKey) { + query.where('permissions.clientkey', clientKey) + } + + const permissions = await query + return permissions + } catch (err) { + throw pgErrorHandler(err) + } +} + module.exports = { insertPermissionByClientId, updatePermissionByPermissionname, @@ -122,4 +155,5 @@ module.exports = { getPermissionsByRoleKey, insertPermission, getPermissionsByNameClientKey, + getPermissions, } diff --git a/database/test/db-admin-permissions.test.js b/database/test/db-admin-permissions.test.js index 8a1310e..9aaf1fb 100644 --- a/database/test/db-admin-permissions.test.js +++ b/database/test/db-admin-permissions.test.js @@ -91,4 +91,47 @@ describe('Admin permissions view', () => { assert.deepEqual(permissions, []) }) }) + + describe('Test GetPermissions by optional filter', () => { + it('Should return all permissions when no filters are applied', async () => { + const { count: permissionCount } = await usherDb('permissions').count('*').first() + const permissions = await adminPermissions.getPermissions() + assert.equal(permissions.length, Number(permissionCount)) + assert.ok(permissionTableColumns.every((col) => col in permissions[0])) + }) + + it('Should return permissions for a specific clientId', async () => { + const { clientkey: clientKey } = await usherDb('permissions').select('clientkey').first() + const { client_id: clientId } = await usherDb('clients').select('client_id').where({ key: clientKey }).first() + const permissions = await adminPermissions.getPermissions({ clientId }) + assert.ok(permissions.length > 0) + assert.ok(permissions.every(permission => permission.client_id === clientId)) + }) + + it('Should return permissions for a specific name', async () => { + const { name } = await usherDb('permissions').select('name').first() + const permissions = await adminPermissions.getPermissions({ name }) + assert.ok(permissions.length > 0) + assert.ok(permissions.every(permission => permission.name === name)) + }) + + it('Should return permissions for a specific clientKey', async () => { + const { clientkey } = await usherDb('permissions').select('clientkey').first() + const permissions = await adminPermissions.getPermissions({ clientKey: clientkey }) + assert.ok(permissions.length > 0) + assert.ok(permissions.every(permission => permission.clientkey === clientkey)) + }) + + it('Should return permissions for multiple filters', async () => { + const { name, clientkey: clientKey } = await usherDb('permissions').select('*').first() + const permissions = await adminPermissions.getPermissions({ name, clientKey }) + assert.ok(permissions.length > 0) + assert.ok(permissions.every(permission => permission.clientkey === clientKey && permission.name === name)) + }) + + it('Should return an empty array if no permissions match the criteria', async () => { + const permissions = await adminPermissions.getPermissions({ name: 'Nonexistent Name', clientId: 'Nonexistent ClientId' }) + assert.ok(permissions.length === 0) + }) + }) }) diff --git a/server/src/api_endpoints/clients/permissions.js b/server/src/api_endpoints/clients/permissions.js index c330f08..f71f0c8 100644 --- a/server/src/api_endpoints/clients/permissions.js +++ b/server/src/api_endpoints/clients/permissions.js @@ -27,6 +27,27 @@ const createPermission = async (req, res, next) => { } } +/** + * HTTP Request handler + * Get a list of permissions for a client + * + * @param {Object} req - The request object + * @param {Object} res - The response object to send a 200 status code and the list of permissions + * @param {Function} next - The next middleware function + * @returns {Promise} - A Promise that resolves to void when permissions are retrieved + */ +const getClientPermissions = async (req, res, next) => { + try { + const { client_id: clientId } = req.params + await checkClientExists(clientId) + const permissions = await dbAdminPermission.getPermissions({ clientId }) + res.status(200).send(permissions) + } catch ({ httpStatusCode = 500, message }) { + return next(createError(httpStatusCode, { message })) + } +} + module.exports = { createPermission, + getClientPermissions, } diff --git a/server/src/api_endpoints/endpoint_permissions.js b/server/src/api_endpoints/endpoint_permissions.js new file mode 100644 index 0000000..fecc666 --- /dev/null +++ b/server/src/api_endpoints/endpoint_permissions.js @@ -0,0 +1,23 @@ +const createError = require('http-errors') +const dbAdminPermission = require('database/layer/admin-permission') + +/** + * HTTP Request handler + * Returns a list of permissions + * + * @param {Object} req - The request object + * @param {Object} res - The response object to send 200 statusCode and a list of permissions + * @param {Function} next - The next middleware function + * @returns {Promise} - A promise that resolves to void when permissions are retrieved + */ +const getPermissions = async (req, res, next) => { + try { + const { name, client_id: clientId, client_key: clientKey } = req.query + const permissions = await dbAdminPermission.getPermissions({ name, clientId, clientKey }) + res.status(200).send(permissions) + } catch ({ httpStatusCode = 500, message }) { + return next(createError(httpStatusCode, { message })) + } +} + +module.exports = { getPermissions } diff --git a/server/test/endpoint_clients.test.js b/server/test/endpoint_clients.test.js index 3b6f6a6..d014e8a 100644 --- a/server/test/endpoint_clients.test.js +++ b/server/test/endpoint_clients.test.js @@ -333,4 +333,43 @@ describe('Admin Clients Endpoint Test', () => { assert.equal(response.status, 409) }) }) + + describe('Get Client Permissions', () => { + let validClientId + const getClientPermissions = async (clientId, header = requestHeaders) => { + return await fetch(`${url}/clients/${clientId}/permissions`, { + method: 'GET', + headers: header, + }) + } + + before(async () => { + const client = await usherDb('clients').select('*').first() + validClientId = client.client_id + }) + + it('should return 200 and list of all permissions', async () => { + const response = await getClientPermissions(validClientId) + assert.equal(response.status, 200) + const permissions = await response.json() + const { count: permissionCount } = await usherDb('permissions') + .join('clients', 'permissions.clientkey', '=', 'clients.key') + .where('clients.client_id', validClientId).count('*').first() + assert.equal(permissions?.length, Number(permissionCount)) + }) + + it('should return 401 due to lack of proper token', async () => { + const userAccessToken = await getTestUser1IdPToken() + const response = await getClientPermissions(validClientId, { + ...requestHeaders, + Authorization: `Bearer ${userAccessToken}` + }) + assert.equal(response.status, 401) + }) + + it('should return 404 for non-existent client id', async () => { + const response = await getClientPermissions('invalid_client_id') + assert.equal(response.status, 404) + }) + }) }) diff --git a/server/test/endpoint_permissions.test.js b/server/test/endpoint_permissions.test.js new file mode 100644 index 0000000..8f89d79 --- /dev/null +++ b/server/test/endpoint_permissions.test.js @@ -0,0 +1,85 @@ +const { describe, it } = require('mocha') +const fetch = require('node-fetch') +const assert = require('node:assert') +const { usherDb } = require('database/layer/knex') +const { getAdmin1IdPToken, getTestUser1IdPToken } = require('./lib/tokens') +const { getServerUrl } = require('./lib/urls') + +describe('Admin Permissions API Tests', () => { + const url = getServerUrl() + let requestHeaders + before(async () => { + const adminAccessToken = await getAdmin1IdPToken() + requestHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${adminAccessToken}`, + } + }) + + describe('GET:/permissions', () => { + /** + * GET /permissions + * HTTP request to retrieve a list of permissions + * + * @param {string} query - The query params to be added to the URL (e.g., ?name=value1&client_id=value2&client_key=value3) + * @param {Object} header - The request headers + * @returns {Promise} - A Promise which resolves to fetch.Response + */ + const getPermissions = async (query = '', header = requestHeaders) => { + return await fetch(`${url}/permissions${query}`, { + method: 'GET', + headers: header, + }) + } + + it('should return 200, return all the permissions', async () => { + const { count: permissionCount } = await usherDb('permissions').count('*').first() + const response = await getPermissions() + assert.equal(response.status, 200) + const permissions = await response.json() + assert.equal(permissions.length, Number(permissionCount)) + }) + + it('should return 200, return all the permissions for a client', async () => { + const { client_id: validClientId, key: validClientKey } = await usherDb('clients').select('*').first() + const { count: permissionCount } = await usherDb('permissions').where({ clientkey: validClientKey }).count('*').first() + const response = await getPermissions(`?client_id=${validClientId}`) + assert.equal(response.status, 200) + const permissions = await response.json() + assert.equal(permissions.length, Number(permissionCount)) + assert.equal(permissions[0]['client_id'], validClientId) + }) + + it('should return 200, return a permission with two filter parameters', async () => { + const validPermission = await usherDb('permissions').select('*').first() + const { clientkey, name } = validPermission + const response = await getPermissions(`?client_key=${clientkey}&name=${name}`) + assert.equal(response.status, 200) + const permissions = await response.json() + assert.ok(permissions.every(permission => permission.clientkey === clientkey)) + assert.ok(permissions.every(permission => permission.name === name)) + }) + + it('should return 200, return an empty array for an invalid client_id', async () => { + const response = await getPermissions('?client_id=invalid') + assert.equal(response.status, 200) + const permissions = await response.json() + assert.equal(permissions.length, 0) + }) + + it('should return 400, due to an invalid query param', async () => { + const response = await getPermissions('?client_key=string,') + assert.equal(response.status, 400) + }) + + it('should return 401, unauthorized token', async () => { + const userAccessToken = await getTestUser1IdPToken() + const response = await getPermissions('', + { + ...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 798855a..2ff3d18 100644 --- a/server/the-usher-openapi-spec.yaml +++ b/server/the-usher-openapi-spec.yaml @@ -496,6 +496,42 @@ paths: 503: $ref: '#/components/responses/ServiceUnavailableError' + /permissions: + get: + 'x-swagger-router-controller': 'endpoint_permissions' + operationId: getPermissions + summary: Get a List of permissions, optionally filtered by name, client_id and client_key + tags: + - Admin APIs + security: + - bearerAdminAuth: [] + parameters: + - $ref: '#/components/parameters/nameQueryParam' + - $ref: '#/components/parameters/clientIdQueryParam' + - $ref: '#/components/parameters/clientKeyQueryParam' + responses: + 200: + description: The List of Permissions + content: + application/json: + schema: + type: array + items: + allOf: + - $ref: '#/components/schemas/PermissionObject' + - type: object + properties: + client_id: + type: string + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/ServiceUnavailableError' + /personas: get: 'x-swagger-router-controller': 'personas/persona' @@ -1061,6 +1097,39 @@ paths: /clients/{client_id}/permissions: parameters: - $ref: '#/components/parameters/clientIdPathParam' + get: + 'x-swagger-router-controller': 'clients/permissions' + operationId: getClientPermissions + summary: Get a list of permissions for a client + tags: + - Client Admin APIs + security: + - bearerAdminAuth: [] + - bearerClientAdminAuth: [] + responses: + 200: + description: List of permissions for a client + content: + application/json: + schema: + type: array + items: + allOf: + - $ref: '#/components/schemas/PermissionObject' + - type: object + properties: + client_id: + 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' post: 'x-swagger-router-controller': 'clients/permissions' summary: Create a new permission for the given client @@ -1183,7 +1252,21 @@ components: value: "*" clientIdQueryParam: name: client_id - description: Unique identifier for the client. + description: Filter by client_id + in: query + required: false + schema: + $ref: '#/components/schemas/EntityNameDef' + clientKeyQueryParam: + name: client_key + description: Filter by client_key + in: query + required: false + schema: + type: integer + nameQueryParam: + name: name + description: Filter by name in: query required: false schema: