diff --git a/src/__tests__/userChangemakerPermissions.int.test.ts b/src/__tests__/userChangemakerPermissions.int.test.ts index c20c7df5..96a41e2d 100644 --- a/src/__tests__/userChangemakerPermissions.int.test.ts +++ b/src/__tests__/userChangemakerPermissions.int.test.ts @@ -3,6 +3,7 @@ import { app } from '../app'; import { createChangemaker, createOrUpdateUserChangemakerPermission, + loadUserChangemakerPermission, } from '../database'; import { expectTimestamp, loadTestUser } from '../test/utils'; import { @@ -10,6 +11,7 @@ import { mockJwtWithAdminRole as authHeaderWithAdminRole, } from '../test/mockJwt'; import { keycloakUserIdToString, Permission } from '../types'; +import { NotFoundError } from '../errors'; describe('/users/changemakers/:changemakerId/permissions/:permission', () => { describe('PUT /', () => { @@ -123,4 +125,166 @@ describe('/users/changemakers/:changemakerId/permissions/:permission', () => { }); }); }); + + describe('DELETE /', () => { + it('returns 401 if the request lacks authentication', async () => { + const user = await loadTestUser(); + const changemaker = await createChangemaker({ + taxId: '11-1111111', + name: 'Example Inc.', + }); + await request(app) + .delete( + `/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`, + ) + .send() + .expect(401); + }); + + it('returns 401 if the authenticated user lacks permission', async () => { + const user = await loadTestUser(); + const changemaker = await createChangemaker({ + taxId: '11-1111111', + name: 'Example Inc.', + }); + await request(app) + .delete( + `/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`, + ) + .set(authHeader) + .send() + .expect(401); + }); + + it('returns 400 if the userId is not a valid keycloak user ID', async () => { + await request(app) + .delete(`/users/notaguid/changemakers/1/permissions/${Permission.MANAGE}`) + .set(authHeaderWithAdminRole) + .send() + .expect(400); + }); + + it('returns 400 if the changemaker ID is not a valid ID', async () => { + const user = await loadTestUser(); + await request(app) + .delete( + `/users/${user.keycloakUserId}/changemakers/notanId/permissions/${Permission.MANAGE}`, + ) + .set(authHeaderWithAdminRole) + .send() + .expect(400); + }); + + it('returns 400 if the permission is not a valid permission', async () => { + const user = await loadTestUser(); + await request(app) + .delete( + `/users/${user.keycloakUserId}/changemakers/1/permissions/notAPermission`, + ) + .set(authHeaderWithAdminRole) + .send() + .expect(400); + }); + + it('returns 404 if the permission does not exist', async () => { + const user = await loadTestUser(); + const changemaker = await createChangemaker({ + taxId: '11-1111111', + name: 'Example Inc.', + }); + await request(app) + .delete( + `/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`, + ) + .set(authHeaderWithAdminRole) + .send() + .expect(404); + }); + + it('deletes the user changemaker permission when the user has administrative credentials', async () => { + const user = await loadTestUser(); + const changemaker = await createChangemaker({ + taxId: '11-1111111', + name: 'Example Inc.', + }); + await createOrUpdateUserChangemakerPermission({ + userKeycloakUserId: user.keycloakUserId, + changemakerId: changemaker.id, + permission: Permission.EDIT, + createdBy: user.keycloakUserId, + }); + const permission = await loadUserChangemakerPermission( + user.keycloakUserId, + changemaker.id, + Permission.EDIT, + ); + expect(permission).toEqual({ + changemakerId: changemaker.id, + createdAt: expectTimestamp, + createdBy: user.keycloakUserId, + permission: Permission.EDIT, + userKeycloakUserId: user.keycloakUserId, + }); + await request(app) + .delete( + `/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.EDIT}`, + ) + .set(authHeaderWithAdminRole) + .send() + .expect(204); + await expect( + loadUserChangemakerPermission( + user.keycloakUserId, + changemaker.id, + Permission.EDIT, + ), + ).rejects.toThrow(NotFoundError); + }); + + it('deletes the user changemaker permission when the user has permission to manage the changemaker', async () => { + const user = await loadTestUser(); + const changemaker = await createChangemaker({ + taxId: '11-1111111', + name: 'Example Inc.', + }); + await createOrUpdateUserChangemakerPermission({ + userKeycloakUserId: user.keycloakUserId, + changemakerId: changemaker.id, + permission: Permission.MANAGE, + createdBy: user.keycloakUserId, + }); + await createOrUpdateUserChangemakerPermission({ + userKeycloakUserId: user.keycloakUserId, + changemakerId: changemaker.id, + permission: Permission.EDIT, + createdBy: user.keycloakUserId, + }); + const permission = await loadUserChangemakerPermission( + user.keycloakUserId, + changemaker.id, + Permission.EDIT, + ); + expect(permission).toEqual({ + changemakerId: changemaker.id, + createdAt: expectTimestamp, + createdBy: user.keycloakUserId, + permission: Permission.EDIT, + userKeycloakUserId: user.keycloakUserId, + }); + await request(app) + .delete( + `/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.EDIT}`, + ) + .set(authHeader) + .send() + .expect(204); + await expect( + loadUserChangemakerPermission( + user.keycloakUserId, + changemaker.id, + Permission.EDIT, + ), + ).rejects.toThrow(NotFoundError); + }); + }); }); diff --git a/src/database/operations/userChangemakerPermissions/index.ts b/src/database/operations/userChangemakerPermissions/index.ts index 5bce400c..b4f2cd8c 100644 --- a/src/database/operations/userChangemakerPermissions/index.ts +++ b/src/database/operations/userChangemakerPermissions/index.ts @@ -1 +1,3 @@ export * from './createOrUpdateUserChangemakerPermission'; +export * from './loadUserChangemakerPermission'; +export * from './removeUserChangemakerPermission'; diff --git a/src/database/operations/userChangemakerPermissions/loadUserChangemakerPermission.ts b/src/database/operations/userChangemakerPermissions/loadUserChangemakerPermission.ts new file mode 100644 index 00000000..598f0903 --- /dev/null +++ b/src/database/operations/userChangemakerPermissions/loadUserChangemakerPermission.ts @@ -0,0 +1,37 @@ +import { db } from '../../db'; +import { NotFoundError } from '../../../errors'; +import { + keycloakUserIdToString, + type Id, + type JsonResultSet, + type KeycloakUserId, + type Permission, + type UserChangemakerPermission, +} from '../../../types'; + +export const loadUserChangemakerPermission = async ( + userKeycloakUserId: KeycloakUserId, + changemakerId: Id, + permission: Permission, +): Promise => { + const result = await db.sql>( + 'userChangemakerPermissions.selectByPrimaryKey', + { + userKeycloakUserId, + changemakerId, + permission, + }, + ); + const object = result.rows[0]?.object; + if (object === undefined) { + throw new NotFoundError(`Entity not found`, { + entityType: 'UserchangemakerPermission', + entityPrimaryKey: { + userKeycloakUserId: keycloakUserIdToString(userKeycloakUserId), + changemakerId, + permission, + }, + }); + } + return object; +}; diff --git a/src/database/operations/userChangemakerPermissions/removeUserChangemakerPermission.ts b/src/database/operations/userChangemakerPermissions/removeUserChangemakerPermission.ts new file mode 100644 index 00000000..aa5a5441 --- /dev/null +++ b/src/database/operations/userChangemakerPermissions/removeUserChangemakerPermission.ts @@ -0,0 +1,32 @@ +import { NotFoundError } from '../../../errors'; +import { keycloakUserIdToString } from '../../../types'; +import { db } from '../../db'; +import type { KeycloakUserId, Permission } from '../../../types'; + +const removeUserChangemakerPermission = async ( + userKeycloakUserId: KeycloakUserId, + changemakerId: string, + permission: Permission, +): Promise => { + const result = await db.sql('userChangemakerPermissions.deleteOne', { + userKeycloakUserId, + permission, + changemakerId, + }); + + if (result.row_count === 0) { + throw new NotFoundError( + 'The item did not exist and could not be deleted.', + { + entityType: 'UserChangemakerPermission', + entityPrimaryKey: { + userKeycloakUserId: keycloakUserIdToString(userKeycloakUserId), + permission, + changemakerId, + }, + }, + ); + } +}; + +export { removeUserChangemakerPermission }; diff --git a/src/database/queries/userChangemakerPermissions/deleteOne.sql b/src/database/queries/userChangemakerPermissions/deleteOne.sql new file mode 100644 index 00000000..55e787e5 --- /dev/null +++ b/src/database/queries/userChangemakerPermissions/deleteOne.sql @@ -0,0 +1,4 @@ +DELETE FROM user_changemaker_permissions + WHERE user_keycloak_user_id = :userKeycloakUserId + AND permission = :permission::permission_t + AND changemaker_id = :changemakerId; diff --git a/src/database/queries/userChangemakerPermissions/selectByPrimaryKey.sql b/src/database/queries/userChangemakerPermissions/selectByPrimaryKey.sql new file mode 100644 index 00000000..2efcaa9c --- /dev/null +++ b/src/database/queries/userChangemakerPermissions/selectByPrimaryKey.sql @@ -0,0 +1,5 @@ +SELECT user_changemaker_permission_to_json(user_changemaker_permissions.*) AS "object" +FROM user_changemaker_permissions +WHERE user_keycloak_user_id = :userKeycloakUserId + AND changemaker_id = :changemakerId + AND permission = :permission diff --git a/src/handlers/userChangemakerPermissionsHandlers.ts b/src/handlers/userChangemakerPermissionsHandlers.ts index ca595cef..1aa36859 100644 --- a/src/handlers/userChangemakerPermissionsHandlers.ts +++ b/src/handlers/userChangemakerPermissionsHandlers.ts @@ -1,4 +1,7 @@ -import { createOrUpdateUserChangemakerPermission } from '../database'; +import { + createOrUpdateUserChangemakerPermission, + removeUserChangemakerPermission, +} from '../database'; import { isAuthContext, isId, @@ -14,6 +17,61 @@ import { } from '../errors'; import type { Request, Response, NextFunction } from 'express'; +const deleteUserChangemakerPermission = ( + req: Request, + res: Response, + next: NextFunction, +): void => { + if (!isAuthContext(req)) { + next(new FailedMiddlewareError('Unexpected lack of auth context.')); + return; + } + + const { userKeycloakUserId, changemakerId, permission } = req.params; + if (!isKeycloakUserId(userKeycloakUserId)) { + next( + new InputValidationError( + 'Invalid userKeycloakUserId parameter.', + isKeycloakUserId.errors ?? [], + ), + ); + return; + } + if (!isId(changemakerId)) { + next( + new InputValidationError( + 'Invalid changemakerId parameter.', + isId.errors ?? [], + ), + ); + return; + } + if (!isPermission(permission)) { + next( + new InputValidationError( + 'Invalid permission parameter.', + isPermission.errors ?? [], + ), + ); + return; + } + + (async () => { + await removeUserChangemakerPermission( + userKeycloakUserId, + changemakerId, + permission, + ); + res.status(204).contentType('application/json').send(); + })().catch((error: unknown) => { + if (isTinyPgErrorWithQueryContext(error)) { + next(new DatabaseError('Error deleting item.', error)); + return; + } + next(error); + }); +}; + const putUserChangemakerPermission = ( req: Request, res: Response, @@ -86,6 +144,7 @@ const putUserChangemakerPermission = ( }; const userChangemakerPermissionsHandlers = { + deleteUserChangemakerPermission, putUserChangemakerPermission, }; diff --git a/src/openapi.json b/src/openapi.json index 4106c141..70823e49 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -2713,6 +2713,52 @@ } } } + }, + "delete": { + "operationId": "deleteUserChangemakerPermission", + "summary": "Deletes a user-changemaker permission.", + "tags": ["Permissions"], + "security": [ + { + "auth": [] + } + ], + "parameters": [ + { + "name": "userKeycloakUserId", + "description": "The keycloak user id of a user.", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "changemakerId", + "description": "The id of a changemaker.", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "permission", + "description": "The permission to be granted.", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": ["manage", "edit", "view"] + } + } + ], + "responses": { + "204": { + "description": "Confirmation of successful deletion." + } + } } } } diff --git a/src/routers/usersRouter.ts b/src/routers/usersRouter.ts index 1a69aa2c..398b458e 100644 --- a/src/routers/usersRouter.ts +++ b/src/routers/usersRouter.ts @@ -15,5 +15,10 @@ usersRouter.put( requireChangemakerPermission(Permission.MANAGE), userChangemakerPermissionsHandlers.putUserChangemakerPermission, ); +usersRouter.delete( + '/:userKeycloakUserId/changemakers/:changemakerId/permissions/:permission', + requireChangemakerPermission(Permission.MANAGE), + userChangemakerPermissionsHandlers.deleteUserChangemakerPermission, +); export { usersRouter };