Skip to content

casl Proof of Concept#1407

Draft
Scott-James-Hurley wants to merge 20 commits intodevelopfrom
test_casl_use_cases
Draft

casl Proof of Concept#1407
Scott-James-Hurley wants to merge 20 commits intodevelopfrom
test_casl_use_cases

Conversation

@Scott-James-Hurley
Copy link
Copy Markdown
Contributor

@Scott-James-Hurley Scott-James-Hurley commented Mar 10, 2026

Description

This is a proof of concept for implementing casl in UOP.
It covers the following use cases:

  • Checking permissions for viewing a proposal
  • Checking if a user can edit a FAP
  • Checking permissions for fetching FAP proposal reviews
  • Showing appropriate menu items for user
  • Viewing, editing, deleting permissions in the frontend

Motivation and Context

STFC is investigating how to offer more granular and customisable permissions for facilities. casl is an Attribute Based Access Control (ABAC) system. It tests whether the user is allowed to do (action) what they're asking to do to a resource (subject), depending on a specified set of criteria (conditions). These tuples of action, subject and condition are to be stored in the database to allow facilities to define them themselves, instead of relying on how their behaviour defined defined in the code.

Design

Database

Two tables have been added: permissions and role_has_permission. permissions defines what action a role can do on a subject and under what conditions. For example, a User Officer can view any proposal, a FAP chair can update a FAP if they chair that particular FAP. role_has_permission ties these permissions to user roles.

Backend

  • A datasource for fetching and manipulating permissions from the DB and turning them into casl abilities has been added.
  • An authorisation class has been added for checking if a user can edit a FAP
  • A new function has been added to knex to allow filtering db queries using casl abilities (fetching only the entries the user is allowed to see)
  • the proposal authorisation class uses casl to see if a user can view a proposal

My initial thought was that an authorisation class is added for every entity where we perform authorisation checks whose role it is to construct the context object, and test the user's permissions (fetched from the permissions datasource) against said context object, returning a true or false if the user is allowed to do what they're asking.

Frontend

A user interface has been added for interacting with permissions. This is very basic and not very user friendly. Ideally we'd abstract away the syntax for conditions and make it as hard as possible for users to define incorrect permissions. Please note that the table has some visual bugs.

Calls.-.Google.Chrome.2026-03-11.09-08-54.mp4

The menu items are shown based on the permissions the user currently has. This use case could simplify the code (currently a react component is defined for each user role) and would be a good candidate for permissions caching as currently a user's permissions are fetched on every page load.

Conclusion

Pros

Cons

  • casl only allows you to pass one context object for use in testing conditions, meaning you have to do some manual work to compare two variables (e.g. checking that the proposer_id on a proposal is the same as the user's ID, see)
  • Gathering all the data you need for a context object can be difficult without either explicit references to user roles in the code or collecting everything you might need into one large context object, potentially incurring a performance hit
  • The above may be an inherent flaw of ABAC that requires us to redesign how we structure data in the backend

Please let me know your thoughts and feedback. If there's a use case you'd like to be explored, please post them here.

import { UpdatePermissionRuleArgs } from '../../resolvers/mutations/UpdatePermissionRuleMutation';
import { PermissionRulesArgs } from '../../resolvers/queries/PermissionsQuery';

export const actions = ['update', 'read', 'delete'] as const;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Instead of just defining actions and subjects as string, I've provided a list of possible actions and subject to try to help with type detection. This is intended as a small, illustrative example

return rules;
}

substituteConditionsValues(conditions: any, object?: any | undefined) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A condition may look like the following in the database:
{ "user.userId": { "$in": "fap.fapChairUserIds" } }

When we convert this into an object the object values will still be string literals instead of the values they are supposed to be (e.g. "fap.fapChairUserIds" instead of [12345]) so we have to do the substitution ourselves, looking for context object key references in the conditions object and replacing them with the actual values. In the provided example, the resulting conditions object would be:
{ 12345: { "$in": [12345] } }

return ['()', []];
}

const [sql, replacements] = interpret(condition, {
Copy link
Copy Markdown
Contributor Author

@Scott-James-Hurley Scott-James-Hurley Mar 10, 2026

Choose a reason for hiding this comment

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

Converts from ast to SQL.

Copy link
Copy Markdown
Contributor

@simonfernandes simonfernandes left a comment

Choose a reason for hiding this comment

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

Looks great👍

Comment on lines +54 to +55
('read', 'fap_proposal_assignment', NULL, false),
('read', 'fap_proposal_assignment', '{"user_id": "userId"}', true),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually I'm not sure if my thinking was right. I can see the fap_reviewer role has these two permissions:

Image

But it looks like only the db permission one (34) is being used?

Comment on lines +48 to +50
('read', 'proposal', '{"isVisitorOfProposal": true}', false),
('read', 'proposal', '{"isDataAccessUserOfProposal": true}', false),
('read', 'proposal', '{"isMemberOfProposal": true}', false),
Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown
Contributor Author

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 OR operator. So here the logic would be: "is the user a visitor of the proposal OR a data access user of the proposal OR a member of the proposal"

We could probably use something like this to see which conditions aren't met.

header: string;
children: React.ReactNode;
}) => {
//caching would be very useful here
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this referring to caching the list of dashboard read permissions for each role so that there's no delay in loading the menu items on switching roles?

proposal.primaryKey
);
permissionContext = {
isMemberOfFapProposal: this.isMemberOfFapProposal(agent, proposal.primaryKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 await forgotten. Happens to me often and usually has me scratching my head for far too long

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants