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 21, 2024
1 parent ffb4d37 commit 7e6a302
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 3 deletions.
156 changes: 156 additions & 0 deletions src/__tests__/userChangemakerPermissions.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import request from 'supertest';
import { app } from '../app';
import {
createChangemaker,
createOrUpdateUserChangemakerPermission,
loadSystemUser,
} 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,
});
});

it('does not update `createdBy`, but returns the user changemaker permission when user has permission to manage the changemaker', async () => {
const user = await loadTestUser();
const systemUser = await loadSystemUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});
await createOrUpdateUserChangemakerPermission({
userKeycloakUserId: user.keycloakUserId,
changemakerId: changemaker.id,
permission: Permission.MANAGE,
createdBy: systemUser.keycloakUserId,
});
const response = await request(app)
.put(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`,
)
.set(authHeader)
.send({})
.expect(201);
expect(response.body).toEqual({
changemakerId: changemaker.id,
createdAt: expectTimestamp,
createdBy: systemUser.keycloakUserId,
permission: Permission.MANAGE,
userKeycloakUserId: user.keycloakUserId,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ INSERT INTO user_changemaker_permissions (
:changemakerId,
:createdBy
)
ON CONFLICT (user_keycloak_user_id, permission, changemaker_id)
DO NOTHING
ON CONFLICT (user_keycloak_user_id, permission, changemaker_id) DO UPDATE
-- We have to do an update in order to return the row, even though this update is pointless
SET user_keycloak_user_id = EXCLUDED.user_keycloak_user_id
RETURNING user_changemaker_permission_to_json(user_changemaker_permissions) AS "object";
93 changes: 93 additions & 0 deletions src/handlers/userChangemakerPermissionsHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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) => {
console.log(error)

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
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 };
Loading

0 comments on commit 7e6a302

Please sign in to comment.