Skip to content

Commit f456a4a

Browse files
authored
feat: implement stfc dynamic roles (#1442)
1 parent 02029e0 commit f456a4a

3 files changed

Lines changed: 117 additions & 62 deletions

File tree

apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Role, Roles } from '../../models/Role';
22
import { dummyUser } from '../mockups/UserDataSource';
3+
import PostgresUserDataSource from '../postgres/UserDataSource';
34
import { StfcUserDataSource } from './StfcUserDataSource';
45

56
jest.mock('../postgres/UserDataSource.ts');
@@ -157,6 +158,12 @@ describe('Role tests', () => {
157158
])
158159
);
159160

161+
const mockGetUserRoles = jest.spyOn(
162+
PostgresUserDataSource.prototype,
163+
'getUserRoles'
164+
);
165+
mockGetUserRoles.mockImplementation(() => Promise.resolve([]));
166+
160167
const mockEnsureDummyUserExists = jest.spyOn(
161168
StfcUserDataSource.prototype,
162169
'ensureDummyUserExists'
@@ -181,7 +188,7 @@ describe('Role tests', () => {
181188
);
182189
});
183190

184-
test('When getting roles for a user, STFC roles are translated into ESS roles', async () => {
191+
test('When getting roles for a user, STFC roles are translated into UO system roles', async () => {
185192
const userdataSource = new StfcUserDataSource();
186193

187194
return expect(

apps/backend/src/datasources/stfc/StfcUserDataSource.ts

Lines changed: 86 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type StfcRolesToEssRole = { [key: string]: Roles[] };
2525
/*
2626
* Must not contain user role, this is appended at the very last step.
2727
*/
28-
const stfcRolesToEssRoleDefinitions: StfcRolesToEssRole = {
28+
const stfcRolesToSystemRoleDefinitions: StfcRolesToEssRole = {
2929
'User Officer': [Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST],
3030
'ISIS Instrument Scientist': [Roles.INSTRUMENT_SCIENTIST],
3131
'CLF Artemis FAP Secretary': [Roles.INSTRUMENT_SCIENTIST],
@@ -395,7 +395,40 @@ export class StfcUserDataSource implements UserDataSource {
395395
}
396396

397397
async setUserRoles(id: number, roles: number[]): Promise<void> {
398-
throw new Error('Method not implemented.');
398+
try {
399+
const roleDefinitions = await this.getRoles();
400+
if (!roleDefinitions) {
401+
throw new Error('Role definitions not set up in the system');
402+
}
403+
const userRole: Role | undefined = roleDefinitions.find(
404+
(role) => role.shortCode == Roles.USER
405+
);
406+
const stfcRoles = await this.getRolesForUser(id);
407+
408+
const assignedRoleIds = new Set(
409+
this.mapStfcRolesToSystemRoleDefinitions(
410+
stfcRoles,
411+
roleDefinitions
412+
).map((r) => r.id)
413+
);
414+
const newRolesToAssign = Array.from(new Set(roles)).filter(
415+
(roleId) => !assignedRoleIds.has(roleId) && roleId !== userRole?.id
416+
);
417+
418+
if (newRolesToAssign.length > 0) {
419+
await postgresUserDataSource.setUserRoles(id, newRolesToAssign);
420+
this.stfcRolesCache.remove(String(id));
421+
this.uopRolesCache.remove(String(id));
422+
}
423+
424+
return;
425+
} catch (error) {
426+
logger.logError('Error setting user roles in', {
427+
error,
428+
userId: id,
429+
});
430+
throw error;
431+
}
399432
}
400433
async getRolesForUser(id: number): Promise<RoleDTO[]> {
401434
const cachedRoles = this.stfcRolesCache.get(String(id));
@@ -417,63 +450,68 @@ export class StfcUserDataSource implements UserDataSource {
417450

418451
return stfcRawRolesRequest!;
419452
}
420-
421453
async getUserRoles(id: number): Promise<Role[]> {
422454
const cachedRoles = this.uopRolesCache.get(String(id));
423455
if (cachedRoles) {
424456
return cachedRoles;
425457
}
426458

427-
const stfcRawRolesRequest = this.getRolesForUser(id);
459+
const roleDefinitions = await this.getRoles();
460+
if (!roleDefinitions) {
461+
return [];
462+
}
428463

429-
const stfcRolesRequest = stfcRawRolesRequest.then((stfcRoles) => {
430-
return this.getRoles().then((roleDefinitions) => {
431-
const userRole: Role | undefined = roleDefinitions.find(
432-
(role) => role.shortCode == Roles.USER
433-
);
434-
if (!userRole) {
435-
return [];
436-
}
464+
const [assignedSystemUserRoles, stfcRoles] = await Promise.all([
465+
postgresUserDataSource.getUserRoles(id),
466+
this.getRolesForUser(id),
467+
]);
437468

438-
if (!stfcRoles || stfcRoles.length == 0) {
439-
return [userRole];
440-
}
469+
const userRole: Role | undefined = roleDefinitions.find(
470+
(role) => role.shortCode == Roles.USER
471+
);
472+
if (!userRole) {
473+
return [];
474+
}
441475

442-
/*
443-
* Convert the STFC roles to the Roles enums which refers to roles
444-
* by short code. We will use the short code to filter relevant
445-
* roles.
446-
*/
447-
const userRolesAsEnum: Roles[] = stfcRoles
448-
.flatMap((stfcRole) => stfcRolesToEssRoleDefinitions[stfcRole.name!])
449-
.filter((r) => r !== undefined) as Roles[];
450-
451-
/*
452-
* Filter relevant roles by short code.
453-
*/
454-
const userRolesAsRole: Role[] = userRolesAsEnum
455-
.map((r) => roleDefinitions.find((d) => d.shortCode === r))
456-
.filter((r) => r !== undefined) as Role[];
457-
458-
/*
459-
* We can't return non-unique roles.
460-
*/
461-
const uniqueRoles: Role[] = [...new Set(userRolesAsRole)];
462-
463-
uniqueRoles.sort((a, b) => a.id - b.id);
464-
465-
/*
466-
* Prepend the user role as it must be first.
467-
*/
468-
const userRoles = [userRole, ...uniqueRoles];
469-
470-
return userRoles;
471-
});
472-
});
476+
if (!stfcRoles || stfcRoles.length == 0) {
477+
return [userRole];
478+
}
479+
480+
const externalRoles = this.mapStfcRolesToSystemRoleDefinitions(
481+
stfcRoles,
482+
roleDefinitions
483+
);
484+
485+
const combinedUserRoles = [...externalRoles, ...assignedSystemUserRoles];
486+
487+
const uniqueRoles: Role[] = [...new Set(combinedUserRoles)];
488+
489+
uniqueRoles.sort((a, b) => a.id - b.id);
490+
491+
const userRoles = [userRole, ...uniqueRoles];
492+
493+
this.uopRolesCache.put(String(id), Promise.resolve(userRoles));
494+
495+
return userRoles;
496+
}
497+
498+
private mapStfcRolesToSystemRoleDefinitions(
499+
stfcRoles: RoleDTO[] | undefined,
500+
roleDefinitions: Role[]
501+
): Role[] {
502+
if (!stfcRoles?.length) {
503+
return [];
504+
}
505+
506+
const userRolesAsEnum: Roles[] = stfcRoles
507+
.flatMap((stfcRole) => stfcRolesToSystemRoleDefinitions[stfcRole.name!])
508+
.filter((r) => r !== undefined) as Roles[];
473509

474-
this.uopRolesCache.put(String(id), stfcRolesRequest);
510+
const mappedRoles: Role[] = userRolesAsEnum
511+
.map((r) => roleDefinitions.find((d) => d.shortCode === r))
512+
.filter((r) => r !== undefined) as Role[];
475513

476-
return stfcRolesRequest;
514+
return mappedRoles;
477515
}
478516

479517
async getRoles(): Promise<Role[]> {

apps/e2e/cypress/e2e/tag.cy.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import initialDBData from '../support/initialDBData';
66

77
const tagName = faker.word.words(2);
88
const tagShortCode = faker.word.words(1);
9-
9+
const dynamicRoleDetails = {
10+
id: 11,
11+
title: 'Dynamic Proposal Reader',
12+
description: 'Role that can only see calls and proposals with specific tags',
13+
shortCode: 'proposal_reader',
14+
};
1015
const scientist1 = initialDBData.users.user1;
1116

1217
const instrument1 = {
@@ -19,10 +24,7 @@ const instrument1 = {
1924
context('Tag tests', () => {
2025
beforeEach(function () {
2126
cy.getAndStoreFeaturesEnabled().then(() => {
22-
if (
23-
!featureFlags.getEnabledFeatures().get(FeatureId.TAGS) ||
24-
!featureFlags.getEnabledFeatures().get(FeatureId.OAUTH) // TODO implement method setUserRoles in StfcUserDataSource and remove this condition
25-
) {
27+
if (!featureFlags.getEnabledFeatures().get(FeatureId.TAGS)) {
2628
this.skip();
2729
}
2830
});
@@ -181,17 +183,17 @@ context('Tag tests', () => {
181183

182184
cy.get('[data-cy="create-new-entry"]').click();
183185

184-
cy.get('[data-cy="role-title-input"]').type('Dynamic Proposal Reader');
186+
cy.get('[data-cy="role-title-input"]').type(dynamicRoleDetails.title);
185187
cy.get('[data-cy="role-description-input"]').type(
186-
'Role that can only see calls and proposals with specific tags'
188+
dynamicRoleDetails.description
187189
);
188190
cy.get('[data-cy="role-shortcode-select"]').click();
189-
cy.get('[role="listbox"]').contains('proposal_reader').click();
191+
cy.get('[role="listbox"]').contains(dynamicRoleDetails.shortCode).click();
190192
cy.get('[data-cy="submit-role-button"]').click();
191193

192194
cy.updateUserRoles({
193195
id: initialDBData.users.user1.id,
194-
roles: [11], // newly created role
196+
roles: [dynamicRoleDetails.id], // newly created role
195197
});
196198

197199
cy.login('user1');
@@ -204,13 +206,21 @@ context('Tag tests', () => {
204206

205207
cy.login('officer');
206208
cy.visit('/admin/roles');
207-
cy.get('[aria-label="Assign Tag"]').click();
209+
cy.finishedLoading();
210+
211+
cy.contains('tr', dynamicRoleDetails.title)
212+
.find('[aria-label="Assign Tags"]')
213+
.click();
208214

209-
cy.get('[role="dialog"]').contains(tagName).click();
215+
cy.get('[role="dialog"] input[placeholder="Select tags"]')
216+
.should('be.visible')
217+
.click()
218+
.type(tagName);
210219

211-
cy.get('[role="dialog"] [data-cy="assign-selected-tags"]').click();
220+
cy.get('[role="option"]').contains(tagName).should('be.visible').click();
212221

213-
cy.login('user1');
222+
cy.get('[role="dialog"] [data-cy="assign-selected-tags"]').click();
223+
cy.login('user1', dynamicRoleDetails.id);
214224
cy.visit('/Proposals');
215225

216226
cy.contains(title).should('exist');

0 commit comments

Comments
 (0)