-
Notifications
You must be signed in to change notification settings - Fork 11
casl Proof of Concept #1407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
casl Proof of Concept #1407
Changes from all commits
37f0f5e
05bb422
f5c012e
e96b057
411e3c8
c1e6054
b423277
7b135f0
96d9f62
ad7c1d7
6814321
0b07b54
1468307
7901188
de386e8
c41dc36
7177a2e
4ab9fb4
558dcaa
a655192
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| DO | ||
| $$ | ||
| BEGIN | ||
| IF register_patch('9998_CaslPOC.sql', 'scotthurley', 'Casl', '2026-01-29') THEN | ||
|
|
||
| BEGIN | ||
| CREATE TABLE IF NOT EXISTS permissions( | ||
| permission_id serial UNIQUE, | ||
| action varchar(100) DEFAULT NULL, | ||
| subject varchar(100) DEFAULT NULL, | ||
| conditions varchar(200) DEFAULT NULL, | ||
| is_db_permission bool DEFAULT false | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS role_has_permission( | ||
| role_id integer NOT NULL REFERENCES roles (role_id) ON UPDATE CASCADE ON DELETE CASCADE, | ||
| permission_id integer NOT NULL REFERENCES permissions (permission_id) ON UPDATE CASCADE ON DELETE CASCADE, | ||
| PRIMARY KEY (role_id, permission_id) | ||
| ); | ||
|
|
||
| INSERT INTO permissions(action, subject, conditions, is_db_permission) VALUES | ||
| ('update', 'fap', '{ "user.userId": { "$in": "fap.fapChairUserIds" } }', false), | ||
| ('archive', 'call', ' { "shortCode": { "$regex": "/LSF/i" }}', false), | ||
| ('delete', 'proposal', '{"proposal.proposerId": "user.userId", "proposal.submitted": false}', false), | ||
| ('read', 'proposal', '{"isInternalReviewer": true}', false), | ||
| ('read', 'proposal', '{"isMemberOfFapProposal": true}', false), | ||
| ('read', 'proposal_dashboard', NULL, false), | ||
| ('delete', 'proposal', NULL, false), | ||
| ('create', 'proposal', NULL, false), | ||
| ('read', 'dashboard', NULL, false), | ||
| ('read', 'experiment_times', NULL, false), | ||
| ('read', 'call', NULL, false), | ||
| ('read', 'proposal', NULL, false), | ||
| ('read', 'permission', NULL, false), | ||
| ('read', 'technique_proposal', NULL, false), | ||
| ('read', 'experiment', NULL, false), | ||
| ('read', 'fap', NULL, false), | ||
| ('read', 'instrument', NULL, false), | ||
| ('read', 'technique', NULL, false), | ||
| ('read', 'tag', NULL, false), | ||
| ('read', 'proposal_workflow', NULL, false), | ||
| ('read', 'institution', NULL, false), | ||
| ('read', 'people', NULL, false), | ||
| ('read', 'question', NULL, false), | ||
| ('read', 'setting', NULL, false), | ||
| ('read', 'status_action_logs', NULL, false), | ||
| ('read', 'template', NULL, false), | ||
| ('read', 'proposal', '{"isVisitorOfProposal": true}', false), | ||
| ('read', 'proposal', '{"isDataAccessUserOfProposal": true}', false), | ||
| ('read', 'proposal', '{"isMemberOfProposal": true}', false), | ||
| ('read', 'proposal', '{"isInstrumentManagerToProposal": true}', false), | ||
| ('read', 'proposal', '{"isScientistToProposalTechnique": true}', false), | ||
| ('read', 'proposal', '{"isScientistToProposal": true}', false), | ||
| ('read', 'fap_proposal_assignment', NULL, false), | ||
| ('read', 'fap_proposal_assignment', '{"user_id": "userId"}', true), | ||
|
Comment on lines
+54
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems nice if I've interpreted it correctly: all of the permission filtering is handled at db level, so that the main check can be written without a condition for simplicity and performance. In Casbin, if the filtering takes place and provides the exact outcome, it's still necessary to collect the context and evaluate each proposal again.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| ('update', 'fap', NULL, false); | ||
|
|
||
| INSERT INTO role_has_permission(role_id, permission_id) VALUES | ||
| (4,1), | ||
| (2,7), | ||
| (1,3), | ||
| (2,2), | ||
| (1,8), | ||
| (1,9), | ||
| (1,10), | ||
| (2,11), | ||
| (2,12), | ||
| (2,13), | ||
| (2,14), | ||
| (2,15), | ||
| (2,16), | ||
| (2,17), | ||
| (2,18), | ||
| (2,19), | ||
| (2,20), | ||
| (2,21), | ||
| (2,22), | ||
| (2,23), | ||
| (4,16), | ||
| (5,16), | ||
| (6,16), | ||
| (7,14), | ||
| (7,15), | ||
| (7,17), | ||
| (8,15), | ||
| (4,9), | ||
| (5,9), | ||
| (6,9), | ||
| (7,9), | ||
| (2,24), | ||
| (2,25), | ||
| (2,26), | ||
| (2,6), | ||
| (7,12), | ||
| (1,29), | ||
| (7,30), | ||
| (9,4), | ||
| (4,5), | ||
| (5,5), | ||
| (6,5), | ||
| (8,12), | ||
| (1,27), | ||
| (1,28), | ||
| (7,32), | ||
| (7,31), | ||
| (2,33), | ||
| (4,33), | ||
| (5,33), | ||
| (6,33), | ||
| (6,34), | ||
| (2,35); | ||
|
|
||
| END; | ||
| END IF; | ||
| END; | ||
| $$ | ||
| LANGUAGE plpgsql; | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { inject, injectable } from 'tsyringe'; | ||
| import { Tokens } from '../config/Tokens'; | ||
| import { PermissionDataSource } from '../datasources/PermissionDataSource'; | ||
| import { UserWithRole } from '../models/User'; | ||
| import { FapDataSource } from '../datasources/FapDataSource'; | ||
|
|
||
| @injectable() | ||
| export class FapAuthorization { | ||
| constructor( | ||
| @inject(Tokens.FapDataSource) | ||
| private fapDataSource: FapDataSource, | ||
| @inject(Tokens.PermissionDataSource) | ||
| private permissionDataSource: PermissionDataSource | ||
| ) {} | ||
|
|
||
| async canEdit(agent: UserWithRole | null, fapId: number) { | ||
| const fap = await this.fapDataSource.getFap(fapId); | ||
|
|
||
| const user = { | ||
| role: agent?.currentRole?.shortCode, | ||
| userId: agent?.id, | ||
| }; | ||
|
|
||
| const ctx = {fap, user} | ||
| return this.permissionDataSource.hasPermission(user.role ? user.role : 'user', 'update', 'fap', false, ctx); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { Proposal } from '../resolvers/types/Proposal'; | |
| import { UserDataSource } from './../datasources/UserDataSource'; | ||
| import { UserJWT } from './../models/User'; | ||
| import { UserAuthorization } from './UserAuthorization'; | ||
| import { PermissionDataSource } from '../datasources/PermissionDataSource'; | ||
|
|
||
| @injectable() | ||
| export class ProposalAuthorization { | ||
|
|
@@ -35,7 +36,9 @@ export class ProposalAuthorization { | |
| private statusDataSource: StatusDataSource, | ||
| @inject(Tokens.DataAccessUsersDataSource) | ||
| private dataAccessUsersDataSource: DataAccessUsersDataSource, | ||
| @inject(Tokens.UserAuthorization) protected userAuth: UserAuthorization | ||
| @inject(Tokens.UserAuthorization) protected userAuth: UserAuthorization, | ||
| @inject(Tokens.PermissionDataSource) | ||
| private permissionDataSource: PermissionDataSource | ||
| ) {} | ||
|
|
||
| private async resolveProposal( | ||
|
|
@@ -258,50 +261,45 @@ export class ProposalAuthorization { | |
|
|
||
| const currentRole = agent?.currentRole?.shortCode; | ||
|
|
||
| let hasAccess = false; | ||
| let permissionContext: any = null; | ||
| let isApiKey = false; | ||
|
|
||
| //I kept the switch statement with explicit reference to roles in order to avoid having to gather all the properties | ||
| //in a single context object, which would affect performance | ||
| switch (currentRole) { | ||
| case Roles.USER: | ||
| hasAccess = | ||
| (await this.isMemberOfProposal(agent, proposal)) || | ||
| (await this.isVisitorOfProposal(agent, proposal.primaryKey)) || | ||
| (await this.isDataAccessUserOfProposal(agent, proposal.primaryKey)); | ||
| permissionContext = { | ||
| isMemberOfProposal: await this.isMemberOfProposal(agent, proposal), | ||
| isVisitorOfProposal: await this.isVisitorOfProposal(agent, proposal.primaryKey), | ||
| isDataAccessUserOfProposal: await this.isDataAccessUserOfProposal(agent, proposal.primaryKey) | ||
| }; | ||
| break; | ||
| case Roles.INSTRUMENT_SCIENTIST: | ||
| hasAccess = | ||
| (await this.isInstrumentManagerToProposal( | ||
| agent, | ||
| proposal.primaryKey | ||
| )) || | ||
| (await this.isScientistToProposal(agent, proposal.primaryKey)) || | ||
| (await this.isScientistToProposalTechnique( | ||
| agent, | ||
| proposal.primaryKey | ||
| )); | ||
| permissionContext = { | ||
| isInstrumentManagerToProposal: await this.isInstrumentManagerToProposal(agent, proposal.primaryKey), | ||
| isScientistToProposal: await this.isScientistToProposal(agent, proposal.primaryKey), | ||
| isScientistToProposalTechnique: await this.isScientistToProposalTechnique(agent, proposal.primaryKey) | ||
| }; | ||
| break; | ||
| case Roles.INTERNAL_REVIEWER: | ||
| hasAccess = await this.isInternalReviewer(agent, proposal.primaryKey); | ||
| permissionContext = { | ||
| isInternalReviewer: await this.isInternalReviewer(agent, proposal.primaryKey) | ||
| }; | ||
| break; | ||
| case Roles.FAP_REVIEWER: | ||
| case Roles.FAP_SECRETARY: | ||
| case Roles.FAP_CHAIR: | ||
| hasAccess = await this.isMemberOfFapProposal( | ||
| agent, | ||
| proposal.primaryKey | ||
| ); | ||
| permissionContext = { | ||
| isMemberOfFapProposal: this.isMemberOfFapProposal(agent, proposal.primaryKey) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case there's any weirdness with this permission - looks like there's an |
||
| }; | ||
| break; | ||
| case Roles.USER_OFFICER: | ||
| hasAccess = true; | ||
| break; | ||
| case Roles.EXPERIMENT_SAFETY_REVIEWER: | ||
| hasAccess = true; | ||
| break; | ||
|
|
||
| default: | ||
| hasAccess = this.userAuth.hasGetAccessByToken(agent); | ||
| isApiKey = true; | ||
| } | ||
|
|
||
| return hasAccess; | ||
| return isApiKey? this.userAuth.hasGetAccessByToken(agent) : await this.permissionDataSource.hasPermission(currentRole ? currentRole : 'user', 'read', 'proposal', false, permissionContext); //probably need to change | ||
| } | ||
|
|
||
| private async isProposalEditable( | ||
|
|
@@ -366,4 +364,16 @@ export class ProposalAuthorization { | |
|
|
||
| return false; | ||
| } | ||
|
|
||
| async canDelete(agent: UserWithRole | null, proposalPk: number) { | ||
| const proposal = await this.proposalDataSource.get(proposalPk); | ||
|
|
||
| const user = { | ||
| role: agent?.currentRole?.shortCode, | ||
| userId: agent?.id, | ||
| }; | ||
|
|
||
| const ctx = {proposal, user} | ||
| return this.permissionDataSource.hasPermission(user.role ? user.role : 'user', 'delete', 'proposal', false, ctx); | ||
| } | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By having conditions for a subject split across separate rows, is it somehow possible to know which particular condition has returned false for clearer error messaging?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
separate rules collected into a single ability object are treated with the
ORoperator. So here the logic would be: "is the user a visitor of the proposalORa data access user of the proposalORa member of the proposal"We could probably use something like this to see which conditions aren't met.