Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/backend/db_patches/0180_CreateUserRoles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
DO
$$
BEGIN
IF register_patch('0180_Create_user_roles.sql', 'Fredrik Bolmsten', 'Create dynamic user roles', '2025-05-06') THEN
BEGIN

ALTER TABLE roles
ADD COLUMN permissions TEXT[] NOT NULL DEFAULT '{}';

ALTER TABLE roles
ADD COLUMN data_access TEXT[] NOT NULL DEFAULT '{}';

END;
END IF;
END;
$$
LANGUAGE plpgsql;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Looks like an empty file

Empty file.
6 changes: 0 additions & 6 deletions apps/backend/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
services:
duo-cron-job:
image: ghcr.io/userofficeproject/user-office-cron:develop
environment:
API_URL: http://host.docker.internal:4000/graphql
API_AUTH_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoyLCJ1c2VyX3RpdGxlIjoiTXIuIiwiZmlyc3RuYW1lIjoiQW5kZXJzIiwibWlkZGxlbmFtZSI6IkFkYW0iLCJsYXN0bmFtZSI6IkFuZGVyc3NvbiIsInVzZXJuYW1lIjoidGVzdG9mZmljZXIiLCJwcmVmZXJyZWRuYW1lIjoiUmhpYW5ub24iLCJvcmNpZCI6Ijg3ODMyMTg5NyIsImdlbmRlciI6Im1hbGUiLCJuYXRpb25hbGl0eSI6IkZyZW5jaCIsImJpcnRoZGF0ZSI6IjE5ODEtMDgtMDRUMjI6MDA6MDAuMDAwWiIsIm9yZ2FuaXNhdGlvbiI6IlBmYW5uZXJzdGlsbCBhbmQgU29ucyIsImRlcGFydG1lbnQiOiJJVCBkZXBhcnRtZW50Iiwib3JnYW5pc2F0aW9uX2FkZHJlc3MiOiJDb25nbywgQWxsZW5ldmlsbGUsIDM1ODIzIE11ZWxsZXIgR2xlbnMiLCJwb3NpdGlvbiI6IkxpYWlzb24iLCJlbWFpbCI6IkFhcm9uX0hhcnJpczQ5QGdtYWlsLmNvbSIsImVtYWlsVmVyaWZpZWQiOnRydWUsInRlbGVwaG9uZSI6IjcxMS0zMTYtNTcyOCIsInRlbGVwaG9uZV9hbHQiOiIxLTM1OS04NjQtMzQ4OSB4NzM5MCIsImNyZWF0ZWQiOiIyMDE5LTEwLTE3VDEwOjU4OjM4LjczNVoiLCJ1cGRhdGVkIjoiMjAxOS0xMC0xN1QxMDo1ODozOC43MzVaIn0sInJvbGVzIjpbeyJpZCI6Miwic2hvcnRDb2RlIjoidXNlcl9vZmZpY2VyIiwidGl0bGUiOiJVc2VyIE9mZmljZXIifV0sImlhdCI6MTU3MTMyNzQ2Mn0.NinmUuwuu0D6syqwd2z5J1BaqhwRPlFaxtML8sA2Ang

db:
image: postgres:16-alpine
restart: always
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/auth/ProposalAuthorization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ test('A instrument sci cannot access proposals they are not on', async () => {
title: 'Instrument Scientist',
shortCode: 'instrument_scientist',
description: '',
permissions: [],
dataAccess: [],
},
},
1
Expand Down
40 changes: 39 additions & 1 deletion apps/backend/src/auth/ProposalAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Tokens } from '../config/Tokens';
import { CallDataSource } from '../datasources/CallDataSource';
import { DataAccessUsersDataSource } from '../datasources/DataAccessUsersDataSource';
import { FapDataSource } from '../datasources/FapDataSource';
import { InstrumentDataSource } from '../datasources/InstrumentDataSource';
import { ProposalDataSource } from '../datasources/ProposalDataSource';
import { ReviewDataSource } from '../datasources/ReviewDataSource';
import { StatusDataSource } from '../datasources/StatusDataSource';
import { VisitDataSource } from '../datasources/VisitDataSource';
import { Roles } from '../models/Role';
import { Role, Roles } from '../models/Role';
import { ProposalStatusDefaultShortCodes } from '../models/Status';
import { UserWithRole } from '../models/User';
import { Proposal } from '../resolvers/types/Proposal';
Expand All @@ -33,6 +34,8 @@ export class ProposalAuthorization {
private callDataSource: CallDataSource,
@inject(Tokens.StatusDataSource)
private statusDataSource: StatusDataSource,
@inject(Tokens.InstrumentDataSource)
private instrumentDataSource: InstrumentDataSource,
@inject(Tokens.DataAccessUsersDataSource)
private dataAccessUsersDataSource: DataAccessUsersDataSource,
@inject(Tokens.UserAuthorization) protected userAuth: UserAuthorization
Expand Down Expand Up @@ -260,6 +263,41 @@ export class ProposalAuthorization {

let hasAccess = false;

// Check data access

const proposalIsntruments =
await this.instrumentDataSource.getInstrumentsByProposalPk(
proposal.primaryKey
);

const rolesArray: Role[] = await this.userDataSource.getUserRoles(agent.id);
const userRoles: Record<string, { dataAccess: string[] }> =
rolesArray.reduce(
(acc, role) => {
acc[role.shortCode] = { dataAccess: role.dataAccess };

return acc;
},
{} as Record<string, { dataAccess: string[] }>
);
proposalIsntruments.forEach((instrument) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: typo

agent.currentRole?.shortCode &&
userRoles[agent.currentRole.shortCode]?.dataAccess.some(
(dataAccess) => {
if (
dataAccess.toLowerCase() === instrument.shortCode.toLowerCase()
) {
hasAccess = true;

return true;
}
}
);
});

if (hasAccess) {
return true;
}
switch (currentRole) {
case Roles.USER:
hasAccess =
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/auth/UserAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export abstract class UserAuthorization {
return agent?.currentRole?.shortCode === Roles.USER_OFFICER;
}

isDynamicProposalReader(agent: UserWithRole | null) {
return agent?.currentRole?.shortCode === Roles.DYNAMIC_PROPOSAL_READER;
}

isExperimentSafetyReviewer(agent: UserWithRole | null) {
return agent?.currentRole?.shortCode === Roles.EXPERIMENT_SAFETY_REVIEWER;
}
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/datasources/ProposalDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export interface ProposalDataSource {
offset?: number,
sortField?: string,
sortDirection?: string,
searchText?: string
searchText?: string,
principleInvestigator?: number[],
instrumentFilter?: string[]
): Promise<{ totalCount: number; proposalViews: ProposalView[] }>;
// Read
get(primaryKey: number): Promise<Proposal | null>;
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/datasources/UserDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Institution } from '../models/Institution';
import { Role, Roles } from '../models/Role';
import { BasicUserDetails, User, UserRole } from '../models/User';
import { AddUserRoleArgs } from '../resolvers/mutations/AddUserRoleMutation';
import { CreateRoleArgs } from '../resolvers/mutations/CreateRoleMutation';
import { CreateUserByEmailInviteArgs } from '../resolvers/mutations/CreateUserByEmailInviteMutation';
import {
UpdateUserByOidcSubArgs,
Expand Down Expand Up @@ -99,6 +100,9 @@ export interface UserDataSource {
): Promise<boolean>;
getRoleByShortCode(roleShortCode: Roles): Promise<Role>;
mergeUsers(fromUserId: number, intoUserId: number): Promise<void>;
createRole(args: CreateRoleArgs): Promise<Role>;
updateRole(args: CreateRoleArgs): Promise<Role>;
deleteRole(id: number): Promise<Role | null>;
getApprovedProposalVisitorsWithInstitution(proposalPk: number): Promise<
{
user: User;
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/datasources/mockups/FapDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,8 @@ export class FapDataSourceMock implements FapDataSource {
shortCode: 'fap_chair',
title: 'Fap Chair',
description: '',
permissions: [],
dataAccess: [],
};
}

Expand Down
85 changes: 80 additions & 5 deletions apps/backend/src/datasources/mockups/UserDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
UserWithRole,
} from '../../models/User';
import { AddUserRoleArgs } from '../../resolvers/mutations/AddUserRoleMutation';
import { CreateRoleArgs } from '../../resolvers/mutations/CreateRoleMutation';
import { CreateUserByEmailInviteArgs } from '../../resolvers/mutations/CreateUserByEmailInviteMutation';
import {
UpdateUserByIdArgs,
Expand Down Expand Up @@ -78,6 +79,8 @@ export const dummyUserOfficerWithRole: UserWithRole = {
title: 'User Officer',
shortCode: 'user_officer',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand Down Expand Up @@ -113,13 +116,22 @@ export const dummyPrincipalInvestigatorWithRole: UserWithRole = {
title: 'Principal investigator',
shortCode: 'user',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};

export const dummyUserWithRole: UserWithRole = {
...dummyUser,
currentRole: { id: 1, title: 'User', shortCode: 'user', description: '' },
currentRole: {
id: 1,
title: 'User',
shortCode: 'user',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};

Expand All @@ -130,6 +142,8 @@ export const dummyFapChairWithRole: UserWithRole = {
title: 'Fap Chair',
shortCode: 'fap_chair',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand All @@ -141,6 +155,8 @@ export const dummyFapSecretaryWithRole: UserWithRole = {
title: 'Fap Secretary',
shortCode: 'fap_secretary',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand All @@ -152,6 +168,8 @@ export const dummyFapReviewerWithRole: UserWithRole = {
title: 'Fap Reviewer',
shortCode: 'fap_reviewer',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand All @@ -163,6 +181,8 @@ export const dummySampleReviewer: UserWithRole = {
title: 'Experiment Safety Reviewer',
shortCode: 'experiment_safety_reviewer',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand All @@ -174,6 +194,8 @@ export const dummyInternalReviewer: UserWithRole = {
title: 'Internal Reviewer',
shortCode: 'internal_reviewer',
description: '',
permissions: [],
dataAccess: [],
},
};

Expand All @@ -185,6 +207,8 @@ export const dummyInstrumentScientist: UserWithRole = {
title: 'Instrument Scientist',
shortCode: 'instrument_scientist',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand All @@ -197,6 +221,8 @@ export const dummyVisitorWithRole: UserWithRole = {
title: 'Visitor',
shortCode: 'user',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};
Expand Down Expand Up @@ -249,11 +275,27 @@ export const dummyUserNotOnProposal = new User(

export const dummyUserNotOnProposalWithRole: UserWithRole = {
...dummyUserNotOnProposal,
currentRole: { id: 1, title: 'User', shortCode: 'user', description: '' },
currentRole: {
id: 1,
title: 'User',
shortCode: 'user',
description: '',
permissions: [],
dataAccess: [],
},
externalTokenValid: true,
};

export class UserDataSourceMock implements UserDataSource {
createRole(args: CreateRoleArgs): Promise<Role> {
throw new Error('Method not implemented.');
}
updateRole(args: CreateRoleArgs): Promise<Role> {
throw new Error('Method not implemented.');
}
deleteRole(id: number): Promise<Role | null> {
throw new Error('Method not implemented.');
}
async delete(id: number): Promise<User | null> {
return dummyUser;
}
Expand Down Expand Up @@ -355,6 +397,8 @@ export class UserDataSourceMock implements UserDataSource {
shortCode: 'user_officer',
title: 'User Officer',
description: '',
permissions: [],
dataAccess: [],
},
];
} else if (id === dummyInstrumentScientist.id) {
Expand All @@ -364,6 +408,8 @@ export class UserDataSourceMock implements UserDataSource {
title: 'Instrument Scientist',
shortCode: 'instrument_scientist',
description: '',
permissions: [],
dataAccess: [],
},
];
} else if (id === 1001) {
Expand All @@ -373,14 +419,32 @@ export class UserDataSourceMock implements UserDataSource {
shortCode: 'fap_reviewer',
title: 'Fap Reviewer',
description: '',
permissions: [],
dataAccess: [],
},
];
} else if (id === dummyFapChairWithRole.id) {
return [
{ id: 4, shortCode: 'fap_chair', title: 'Fap Chair', description: '' },
{
id: 4,
shortCode: 'fap_chair',
title: 'Fap Chair',
description: '',
permissions: [],
dataAccess: [],
},
];
} else {
return [{ id: 2, shortCode: 'user', title: 'User', description: '' }];
return [
{
id: 2,
shortCode: 'user',
title: 'User',
description: '',
permissions: [],
dataAccess: [],
},
];
}
}

Expand All @@ -391,8 +455,17 @@ export class UserDataSourceMock implements UserDataSource {
shortCode: 'user_officer',
title: 'User Officer',
description: '',
permissions: [],
dataAccess: [],
},
{
id: 2,
shortCode: 'user',
title: 'User',
description: '',
permissions: [],
dataAccess: [],
},
{ id: 2, shortCode: 'user', title: 'User', description: '' },
];
}

Expand Down Expand Up @@ -562,6 +635,8 @@ export class UserDataSourceMock implements UserDataSource {
shortCode: 'user_officer',
title: 'User Officer',
description: '',
permissions: [],
dataAccess: [],
};
}

Expand Down
Loading
Loading