From cd53fa68545549908a4ebea1a3e7ebb8eb1edd6e Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Thu, 19 Dec 2024 18:13:44 -0500 Subject: [PATCH 1/7] feat: topic/k1ch/introduce API - GET/permissions --- database/layer/admin-permission.js | 34 ++++++++++++ .../src/api_endpoints/endpoint_permissions.js | 23 ++++++++ server/the-usher-openapi-spec.yaml | 52 ++++++++++++++++++- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 server/src/api_endpoints/endpoint_permissions.js diff --git a/database/layer/admin-permission.js b/database/layer/admin-permission.js index ef910b6..4db2e65 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 {string} [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/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/the-usher-openapi-spec.yaml b/server/the-usher-openapi-spec.yaml index 798855a..fbfe6f5 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' @@ -1183,7 +1219,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: From 3b169f7047ef9e80d7ea22abce2f4a5286929e59 Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Fri, 20 Dec 2024 16:52:56 -0500 Subject: [PATCH 2/7] feat: topic/k1ch/ introduce API / GET/clients/{id}/permissions --- .../src/api_endpoints/clients/permissions.js | 21 ++++++++++++ server/the-usher-openapi-spec.yaml | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+) 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/the-usher-openapi-spec.yaml b/server/the-usher-openapi-spec.yaml index fbfe6f5..0df87c2 100644 --- a/server/the-usher-openapi-spec.yaml +++ b/server/the-usher-openapi-spec.yaml @@ -1139,6 +1139,39 @@ paths: $ref: '#/components/responses/InternalError' 503: $ref: '#/components/responses/ServiceUnavailableError' + 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' /sessions: delete: From 51190855653e5be5828c180cbe87f6fd37aa110f Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Fri, 20 Dec 2024 17:33:41 -0500 Subject: [PATCH 3/7] chore: db-admin-permissions / add tests for getPermissions --- database/test/db-admin-permissions.test.js | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/database/test/db-admin-permissions.test.js b/database/test/db-admin-permissions.test.js index 8a1310e..ccd819c 100644 --- a/database/test/db-admin-permissions.test.js +++ b/database/test/db-admin-permissions.test.js @@ -91,4 +91,46 @@ 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 permissions = await adminPermissions.getPermissions() + assert.ok(permissions.length > 0) + assert.ok(permissionTableColumns.every((col) => col in permissions[0])) + }) + + it('Should return permissions for a specific clientId', async () => { + const { client_id: clientId } = await usherDb('clients').select('client_id').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 { client_id: clientId } = await usherDb('clients').select('client_id').first() + const { name } = await usherDb('permissions').select('name').first() + const permissions = await adminPermissions.getPermissions({ clientId, name }) + assert.ok(permissions.length > 0) + assert.ok(permissions.every(permission => permission.client_id.includes(clientId) && permission.name.includes(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) + }) + }) }) From 1ee10b134ab9aa6af2eee4fa840ad107a54fff15 Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Fri, 20 Dec 2024 18:05:54 -0500 Subject: [PATCH 4/7] chore: k1ch / introduce test for GET:/permissions --- server/test/endpoint_permissions.test.js | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 server/test/endpoint_permissions.test.js diff --git a/server/test/endpoint_permissions.test.js b/server/test/endpoint_permissions.test.js new file mode 100644 index 0000000..4c8871d --- /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 Roles 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: totalCount } = await usherDb('permissions').count('*').first() + const response = await getPermissions() + assert.equal(response.status, 200) + const permissions = await response.json() + assert.equal(permissions.length, Number(totalCount)) + }) + + 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: totalCount } = 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(totalCount)) + 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 empty array for 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 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) + }) + }) +}) From 24e5bd0ade4d757663e093fa8899fcb7d14d30db Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Fri, 20 Dec 2024 18:21:55 -0500 Subject: [PATCH 5/7] chore: k1ch / add test for GET:/clients/{client_id}/permissions --- server/test/endpoint_clients.test.js | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) 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) + }) + }) }) From aec5cdd51887d292112c2e432bdf84df0ff413eb Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Fri, 20 Dec 2024 19:17:22 -0500 Subject: [PATCH 6/7] chore: minor changes --- database/layer/admin-permission.js | 2 +- database/test/db-admin-permissions.test.js | 13 +++-- server/test/endpoint_permissions.test.js | 18 +++--- server/the-usher-openapi-spec.yaml | 66 +++++++++++----------- 4 files changed, 50 insertions(+), 49 deletions(-) diff --git a/database/layer/admin-permission.js b/database/layer/admin-permission.js index 4db2e65..239b17f 100644 --- a/database/layer/admin-permission.js +++ b/database/layer/admin-permission.js @@ -120,7 +120,7 @@ const getPermissionsByNameClientKey = async (name, clientKey) => { * @param {Object} filters - The filters to apply * @param {string} [filters.name] - The name of the permission * @param {string} [filters.clientId] - The client id - * @param {string} [filters.clientKey] - The client key + * @param {number} [filters.clientKey] - The client key * @returns {Promise>} - A promise that resolves to an array of permissions */ const getPermissions = async (filters = {}) => { diff --git a/database/test/db-admin-permissions.test.js b/database/test/db-admin-permissions.test.js index ccd819c..9aaf1fb 100644 --- a/database/test/db-admin-permissions.test.js +++ b/database/test/db-admin-permissions.test.js @@ -94,13 +94,15 @@ describe('Admin permissions view', () => { 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.ok(permissions.length > 0) + 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 { client_id: clientId } = await usherDb('clients').select('client_id').first() + 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)) @@ -121,11 +123,10 @@ describe('Admin permissions view', () => { }) it('Should return permissions for multiple filters', async () => { - const { client_id: clientId } = await usherDb('clients').select('client_id').first() - const { name } = await usherDb('permissions').select('name').first() - const permissions = await adminPermissions.getPermissions({ clientId, name }) + 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.client_id.includes(clientId) && permission.name.includes(name))) + assert.ok(permissions.every(permission => permission.clientkey === clientKey && permission.name === name)) }) it('Should return an empty array if no permissions match the criteria', async () => { diff --git a/server/test/endpoint_permissions.test.js b/server/test/endpoint_permissions.test.js index 4c8871d..8f89d79 100644 --- a/server/test/endpoint_permissions.test.js +++ b/server/test/endpoint_permissions.test.js @@ -5,7 +5,7 @@ const { usherDb } = require('database/layer/knex') const { getAdmin1IdPToken, getTestUser1IdPToken } = require('./lib/tokens') const { getServerUrl } = require('./lib/urls') -describe('Admin Roles API Tests', () => { +describe('Admin Permissions API Tests', () => { const url = getServerUrl() let requestHeaders before(async () => { @@ -21,9 +21,9 @@ describe('Admin Roles API Tests', () => { * 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 {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 + * @returns {Promise} - A Promise which resolves to fetch.Response */ const getPermissions = async (query = '', header = requestHeaders) => { return await fetch(`${url}/permissions${query}`, { @@ -33,20 +33,20 @@ describe('Admin Roles API Tests', () => { } it('should return 200, return all the permissions', async () => { - const { count: totalCount } = await usherDb('permissions').count('*').first() + 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(totalCount)) + 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: totalCount } = await usherDb('permissions').where({ clientkey: validClientKey }).count('*').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(totalCount)) + assert.equal(permissions.length, Number(permissionCount)) assert.equal(permissions[0]['client_id'], validClientId) }) @@ -60,14 +60,14 @@ describe('Admin Roles API Tests', () => { assert.ok(permissions.every(permission => permission.name === name)) }) - it('should return 200, return empty array for invalid client_id', async () => { + 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 invalid query param', async () => { + it('should return 400, due to an invalid query param', async () => { const response = await getPermissions('?client_key=string,') assert.equal(response.status, 400) }) diff --git a/server/the-usher-openapi-spec.yaml b/server/the-usher-openapi-spec.yaml index 0df87c2..2ff3d18 100644 --- a/server/the-usher-openapi-spec.yaml +++ b/server/the-usher-openapi-spec.yaml @@ -1097,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 @@ -1139,39 +1172,6 @@ paths: $ref: '#/components/responses/InternalError' 503: $ref: '#/components/responses/ServiceUnavailableError' - 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' /sessions: delete: From ef0e3c5c95119aaa63b5b4356974ee546dd5dc02 Mon Sep 17 00:00:00 2001 From: Keyvan Chamani Date: Fri, 20 Dec 2024 19:32:21 -0500 Subject: [PATCH 7/7] chore: k1ch / fix test GH workflow --- .github/workflows/run-tests.yaml | 1 + 1 file changed, 1 insertion(+) 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