Skip to content

Commit

Permalink
Add PUT endpoint for userChangemakerPermission
Browse files Browse the repository at this point in the history
This endpoint allows the creation of new user changemaker permissions.
It is also the first endpoint to actually require a particular
changemaker permission in order to access the functionality.

Issue #1250 Support associations between users and organizational entities
  • Loading branch information
slifty committed Nov 14, 2024
1 parent ffb4d37 commit 10e537a
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 1 deletion.
126 changes: 126 additions & 0 deletions src/__tests__/userChangemakerPermissions.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import request from 'supertest';
import { app } from '../app';
import {
createChangemaker,
createOrUpdateUserChangemakerPermission,
} from '../database';
import { expectTimestamp, loadTestUser } from '../test/utils';
import {
mockJwt as authHeader,
mockJwtWithAdminRole as authHeaderWithAdminRole,
} from '../test/mockJwt';
import { keycloakUserIdToString, Permission } from '../types';

describe('/users/changemakers/:changemakerId/permissions/:permission', () => {
describe('PUT /', () => {
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)
.put(
`/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)
.put(
`/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)
.put(`/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)
.put(
`/users/${keycloakUserIdToString(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)
.put(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/1/permissions/notAPermission`,
)
.set(authHeaderWithAdminRole)
.send({})
.expect(400);
});

it('creates and returns the new user changemaker permission when user has administrative credentials', async () => {
const user = await loadTestUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});

const response = await request(app)
.put(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.EDIT}`,
)
.set(authHeaderWithAdminRole)
.send({})
.expect(201);
expect(response.body).toEqual({
changemakerId: changemaker.id,
createdAt: expectTimestamp,
createdBy: user.keycloakUserId,
permission: Permission.EDIT,
userKeycloakUserId: user.keycloakUserId,
});
});

it('creates and returns the new user changemaker permission when 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,
});
const response = await request(app)
.put(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.EDIT}`,
)
.set(authHeader)
.send({})
.expect(201);
expect(response.body).toEqual({
changemakerId: changemaker.id,
createdAt: expectTimestamp,
createdBy: user.keycloakUserId,
permission: Permission.EDIT,
userKeycloakUserId: user.keycloakUserId,
});
});
});
});
92 changes: 92 additions & 0 deletions src/handlers/userChangemakerPermissionsHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { createOrUpdateUserChangemakerPermission } from '../database';
import {
isAuthContext,
isId,
isKeycloakUserId,
isPermission,
isTinyPgErrorWithQueryContext,
isWritableUserChangemakerPermission,
} from '../types';
import {
DatabaseError,
FailedMiddlewareError,
InputValidationError,
} from '../errors';
import type { Request, Response, NextFunction } from 'express';

const putUserChangemakerPermission = (
req: Request,
res: Response,
next: NextFunction,
): void => {
if (!isAuthContext(req)) {
next(new FailedMiddlewareError('Unexpected lack of auth context.'));
return;

Check warning on line 24 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L23-L24

Added lines #L23 - L24 were not covered by tests
}

const { userKeycloakUserId, changemakerId, permission } = req.params;
const createdBy = req.user.keycloakUserId;

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;
}
if (!isWritableUserChangemakerPermission(req.body)) {
next(

Check warning on line 58 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L58

Added line #L58 was not covered by tests
new InputValidationError(
'Invalid request body.',
isWritableUserChangemakerPermission.errors ?? [],
),
);
return;

Check warning on line 64 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L64

Added line #L64 was not covered by tests
}

(async () => {
const userChangemakerPermission =
await createOrUpdateUserChangemakerPermission({
userKeycloakUserId,
changemakerId,
permission,
createdBy,
});
res
.status(201)
.contentType('application/json')
.send(userChangemakerPermission);
})().catch((error: unknown) => {

Check warning on line 79 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L79

Added line #L79 was not covered by tests
if (isTinyPgErrorWithQueryContext(error)) {
next(new DatabaseError('Error creating item.', error));
return;

Check warning on line 82 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L81-L82

Added lines #L81 - L82 were not covered by tests
}
next(error);

Check warning on line 84 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L84

Added line #L84 was not covered by tests
});
};

const userChangemakerPermissionsHandlers = {
putUserChangemakerPermission,
};

export { userChangemakerPermissionsHandlers };
1 change: 1 addition & 0 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './errorHandler';
export * from './processJwt';
export * from './requireAdministratorRole';
export * from './requireAuthentication';
export * from './requireChangemakerPermission';
38 changes: 38 additions & 0 deletions src/middleware/requireChangemakerPermission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Response, NextFunction } from 'express';
import { InputValidationError, UnauthorizedError } from '../errors';
import { isAuthContext, isId } from '../types';
import type { Permission } from '../types';
import type { Request } from 'express';

const requireChangemakerPermission =
(permission: Permission) =>
(req: Request, res: Response, next: NextFunction) => {
if (!isAuthContext(req)) {
next(new UnauthorizedError('The request lacks an AuthContext.'));
return;
}
if (req.role?.isAdministrator === true) {
next();
return;
}
const { changemakerId } = req.params;
if (!isId(changemakerId)) {
next(

Check warning on line 20 in src/middleware/requireChangemakerPermission.ts

View check run for this annotation

Codecov / codecov/patch

src/middleware/requireChangemakerPermission.ts#L20

Added line #L20 was not covered by tests
new InputValidationError('Invalid changemakerId.', isId.errors ?? []),
);
return;

Check warning on line 23 in src/middleware/requireChangemakerPermission.ts

View check run for this annotation

Codecov / codecov/patch

src/middleware/requireChangemakerPermission.ts#L23

Added line #L23 was not covered by tests
}
const { user } = req;
const permissions = user.permissions.changemaker[changemakerId] ?? [];
if (!permissions.includes(permission)) {
next(
new UnauthorizedError(
'Authenticated user does not have permission to perform this action.',
),
);
return;
}
next();
};

export { requireChangemakerPermission };
84 changes: 84 additions & 0 deletions src/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,35 @@
"required": ["entries"]
}
]
},
"UserChangemakerPermission": {
"type": "object",
"properties": {
"permission": {
"$ref": "#/components/schemas/Permission",
"readOnly": true
},
"changemakerId": {
"type": "integer",
"readOnly": true
},
"userKeycloakUserId": {
"type": "string",
"format": "uuid",
"readOnly": true
},
"createdBy": {
"type": "string",
"format": "uuid",
"readOnly": true
},
"createdAt": {
"type": "string",
"format": "date-time",
"readOnly": true
}
},
"required": ["id", "", "createdAt"]
}
}
},
Expand Down Expand Up @@ -2630,6 +2659,61 @@
}
}
}
},
"/users/{userKeycloakUserId}/changemakers/{changemakerId}/permissions/{permission}": {
"put": {
"operationId": "createOrUpdateUserChangemakerPermission",
"summary": "Creates or updates 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": {
"201": {
"description": "The resulting permission.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserChangemakerPermission"
}
}
}
}
}
}
}
}
}
12 changes: 11 additions & 1 deletion src/routers/usersRouter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import express from 'express';
import { userChangemakerPermissionsHandlers } from '../handlers/userChangemakerPermissionsHandlers';
import { usersHandlers } from '../handlers/usersHandlers';
import { requireAuthentication } from '../middleware';
import {
requireAuthentication,
requireChangemakerPermission,
} from '../middleware';
import { Permission } from '../types';

const usersRouter = express.Router();

usersRouter.get('/', requireAuthentication, usersHandlers.getUsers);
usersRouter.put(
'/:userKeycloakUserId/changemakers/:changemakerId/permissions/:permission',
requireChangemakerPermission(Permission.MANAGE),
userChangemakerPermissionsHandlers.putUserChangemakerPermission,
);

export { usersRouter };

0 comments on commit 10e537a

Please sign in to comment.