Skip to content

Commit

Permalink
Topic /k1ch/ introduce API PUT:/roles/{role_key}/permissions (#132)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
k1ch authored Nov 10, 2024
1 parent f77fd98 commit 6f13db9
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 4 deletions.
55 changes: 54 additions & 1 deletion database/layer/admin-rolepermissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>} permissionKeys - An array of permission keys
* @returns {Promise<Array>} - 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<Object[]>} - 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,
}
93 changes: 93 additions & 0 deletions database/test/db-admin-rolepermissions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
29 changes: 28 additions & 1 deletion server/src/api_endpoints/roles/permissions.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<void>} - 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,
}
19 changes: 19 additions & 0 deletions server/src/api_endpoints/roles/utils.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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,
}
86 changes: 86 additions & 0 deletions server/test/endpoint_admin_roles_permissions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
})
40 changes: 38 additions & 2 deletions server/the-usher-openapi-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down

0 comments on commit 6f13db9

Please sign in to comment.