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 13, 2024
1 parent 17193d4 commit 6253648
Show file tree
Hide file tree
Showing 5 changed files with 268 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;
}

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(
new InputValidationError(
'Invalid request body.',
isWritableUserChangemakerPermission.errors ?? [],
),
);
return;
}

(async () => {
const userChangemakerPermission =
await createOrUpdateUserChangemakerPermission({
userKeycloakUserId,
changemakerId,
permission,
createdBy,
});
res
.status(201)
.contentType('application/json')
.send(userChangemakerPermission);
})().catch((error: unknown) => {
if (isTinyPgErrorWithQueryContext(error)) {
next(new DatabaseError('Error creating item.', error));
return;
}
next(error);
});
};

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(
new InputValidationError('Invalid changemakerId.', isId.errors ?? []),
);
return;
}
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 };
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 6253648

Please sign in to comment.