Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

CASL Integration

Erik Roberts edited this page Oct 24, 2023 · 20 revisions

Glimpse uses CASL as the foundation for its authorization system. CASL allows for highly granular control over a User's permissions, with the ability to control access to individual properties on specific objects. CASL is designed to allow you to get as specific as you desire in your permissions system. We take full advantage of all of CASL's features, giving administrators the power to decide exactly what a user (or group of users) can and cannot do.

ℹ️ When we talk about a "user" in this article, we are also referring to clients which have not logged in. These "users" will have guest permissions.

This article goes over the basics of how CASL and CASL's Prisma integration work, however for more in-depth details, it's highly recommended you take a look at the CASL documentation, which itself is quite brief.

Abilities

Abilities are a set of permissions (a.k.a. rules). Whenever a user submits a query to our API, their GlimpseAbility is generated from the database. Currently, these abilities are generated from scratch for each request, however as our needs scale, it may make sense to cache abilities for a short period of time (Or at least the "Guest" ability).

A user's permissions are determined both by the permissions which are applied to them directly, as well as permissions inherited from groups which they are a part of. The order at which groups are applied corresponds to the group's priority, with the lowest priority being applied first, so higher priority groups will overwrite them if necessary.

In most scenarios, group priority will have no impact on a user's permissions. Since permissions are usually additive, the order in which they are applied has no effect. In rare cases, permissions can be inverted, which is the only scenario where order matters. For more information, see the Inverted section.

Groups can also have a parent group. Any permissions from the parent group will be inherited by the child group(s). Similar to priorities, a parent group's permissions will always be applied before the child groups' permissions, allowing children groups to override parent group permissions.

Permissions

Each permission within an ability has up to six properties, however we generally only use four.

Action

Actions are the verb that is being applied by the user. For example, you are creating a Production, or reading a Category. CASL has no restrictions on what can or cannot be an action (they are simple strings), however Glimpse does. Glimpse currently supports six types of actions:

  • create
  • read
  • update
  • delete
  • sort
  • filter

CASL has a built-in special action, manage, which grants permission to do all actions. It is equivalent to creating six permissions for each of the above actions.

Subject

The subject of a permission is the entity or type of entity which is being acted upon. For example, you are creating a Production, or reading a Category. Similar to actions, CASL imposes no restrictions on what can or cannot be a subject. In Glimpse, possible subjects are defined as all available data types within the API. Technically, this is not enforced, and administrators may put any string in as a permission subject. However, this is not recommended, as it will unnecessarily fill up the user's permissions list and the permission(s) will have no effect on any of the user's requests.

There is an important distinction to be made between "subjects" and "subject types":

  • Subject type - A subject type is the type of entity that a permission is applying to. For example, Production, Category, and Person are all subject types used within Glimpse.
  • Subject - A subject is an instance of a subject type. For example, the Productions with id equal to 1, 2, and 3 are all the same subject type, but they are different subjects (they have different values within them).

Within the Glimpse database, "subjects" are always in fact subject types. To apply permissions for a specific subject, you must use permission conditions. Subject types are formatted as simple strings within the database. Throughout this wiki, "subjects" will often be used to synonymously refer to both the subject types and the subjects themselves.

Similar to actions, CASL provides a built-in subject type, all, which grants permission to perform the corresponding action on all subject types.

Unlike actions, however, individual permissions may grant permission to multiple subject types, as the subject property of permissions is an array of strings. Attempting to assign a single string as a subject will generate an error. If you wish to grant permission to a single subject type, you should instead define it as an array with a single string, e.g. ["Production"].

Fields

Often, we do not want to grant permission to all properties on a subject. For example, we may want a guest user to be able to read the name, description, and videos properties of a Production, however we don't want them to be able to read the teamNotes or closetLocation properties. Within CASL, these are referred to as fields, and each permission can be defined to grant access to all fields or only a subset of fields.

Field-based restrictions are applied as an array of strings, where each string is the name of the field that the user can access (or in the case of inverted permissions, cannot access).

Field-based restrictions apply to both actual and virtual properties.

Conditions

With the subject value on permissions, we're able to control the subject type that a user has access to, but not the specific subject(s). Conditions allow us to do this by applying logical rules that must be met in order for the permission (rule) to pass. Conditions are formatted in simple JSON. Thanks to the @casl/prisma package, we're able to apply conditions using the same filter conditions scheme that is available in Prisma. You can see a list of available operators here.

Variables

The Glimpse API currently supports three variables which can be used within its conditions. These variables will be automatically replaced with the correct value when the user's ability is generated.

  • $id - Replaced with the currently logged in user's ID. If the user is not logged in (i.e., is a guest), then this will be replaced with null. Generally, the $id variable should never be used within guest permissions.
  • $groups - Replaced with an array of the IDs of groups which the user is a member of. If the user is not logged in (i.e., is a guest), then this will be replaced with an array that just contains the ID of the guest group.
  • $now - Replaced with a Date object corresponding to the current date/time.

Variables can be escaped by putting a \ before the $ character. For example, \$id will be replaced with $id.

accessibleBy Method

This article would not be complete without at least briefly mentioning the accessibleBy method, exported by @casl/prisma. This method will let you easily generate a Prisma conditional input based off of a user's ability. For more information, check out the @casl/prisma documentation.

⚠️ This method does not take fields into account! It generates a Prisma input which will allow you to filter out objects which the user has permission to perform the given action on some field of the given subject type. Field checks still need to be performed manually.

Inverted

Sometimes, you might want to "invert" a permission, making it deny access to a given resource, instead of allowing it. Note that by default, an empty ability will be completely denying anyway. To add an inverted permission to an empty ability will have no effect. Instead, inversion is used to overwrite an already existing permission. For example, consider a scenario where you want a user to have permission to do everything except delete productions. You could do this by granting the user the following permissions:

[
  { 
    "action": "manage",
    "subject": "all"
  }, {
    "action": "delete",
    "subject": "Production",
    "inverted": true
  }
]

In general, this is not the recommended way to do this, as it is simply too prone to error. Instead, it's recommended that you create the necessary permissions to grant access to all of the subject types that you want without inversion.

With that said, there may be select few use cases where using inverted permissions is the appropriate course of action. For example, if you want "group A" to inherit all but one permission from "group B", it may make the most sense to make group B the parent of group A, and then add an inverted permission to group A. The alternative would likely be to duplicate almost all of the permissions between groups A and B (which is not necessarily a bad thing).

Use inverted permissions sparingly, and be aware of their caveats. Unlike normal permissions, the order in which inverted permissions are applied is important, and is one of the major reasons why they aren't recommended. When applying inverted permissions, Glimpse will first apply all of the normal permissions for the given scope, and then apply all inverted permissions. A more in-depth example can be found below.

Reason

If a permission is inverted, when the permission check fails, this is the reason that will be provided internally by CASL. Glimpse does not currently expose this value, and as such, it is not used. However, it has been added to the permissions data types for the sake of future proofing.

Usage

When the user's ability is generated, it is assigned to Express.Request#permissions, so you can generally refer to it via ctx.req.permissions, or simply req.permissions, depending on the context. To check whether the user has permission for a given action and subject, we use the can method:

const canReadProductions = ctx.req.permissions.can(AbilityActions.Read, "Production");

This method will return true if the user has permission to read any field on any production. In other words, the fields array and conditions object are completely ignored by this method. To check whether the user has permission to read a specific field, we can pass that in as a third parameter:

const canReadProductionNames = ctx.req.permissions.can(AbilityActions.Read, "Production", "name");

However, this still does not take any conditions into account and is simply checking if the user has permission to read the name of any Production. To use conditions, we have to pass in an actual subject, instead of just the subject type, which is where the subject method comes into play.

subject Method

When checking a user's permissions, CASL exports a subject method which can be used to apply a subject type to an object, formally defining it as a subject. In JavaScript, not every object has a class or type associated with it: it is just an object with properties. Therefore, when we pass in an object to check conditions, CASL needs to be informed somehow of what subject type an object is. This can perhaps be best explained with a simple example. In the below example, assume the user has the ability to read any Production which has "Women's Hockey" in the name, but nothing else.

const womensProduction = { name: "Women's Hockey vs. Harvard" };
const mensProduction = { name: "Men's Hockey vs. Yale" };

// True -- The user has permission to read _some_ Production.
ctx.req.permissions.can(AbilityActions.read, "Production");

// Both false -- These are unknown objects, and the user only has permission to read Productions.
ctx.req.permissions.can(AbilityActions.Read, womensProduction);
ctx.req.permissions.can(AbilityActions.Read, mensProduction);

// True -- This is a Production which matches the conditions set within the user's ability.
ctx.req.permissions.can(AbilityActions.Read, subject("Production", womensProduction));

// False -- This is a Production which does NOT the conditions set within the user's ability.
ctx.req.permissions.can(AbilityActions.Read, subject("Production", mensProduction));

ℹ️ CASL will attempt to infer a subject type based on it's class name, but not all objects are constructed from classes. Using subject is recommended even when you are confident that an object was constructed from a class.

Putting it all together

Now that you know the format of permissions and abilities, as well as how they are used in code, let's take a look at a more complex example of an ability, how it is constructed, and what it entails for what the user is or is not allowed to do. Let's start by defining some data.

Users

ID Name
1 John

Groups

ID Name Parent Priority
1 Member NULL 0
2 Admin 2 0
3 Alumni NULL 10

User Groups

ID User ID Group ID
1 1 2
2 1 3

Here we've defined our user, John, as well as some groups. John is a member of the Admin group, which is a child of the Member group, so he inherits all of the permissions of the Member group. He is also a member of the Alumni group, which is a completely separate group with its own permissions. Let's now define some permissions for John.

User Permissions

ID User ID Action Subject Fields Conditions Inverted Reason
1 1 read ["Image", "Video"] NULL {"name": { "contains": "John" } } NULL

John by himself has a single permission, which allows him to read any Image or Video which has "John" in the name. This isn't the only permission that John has, however. He also inherits permissions from the groups that he is a member of. Let's outline those.

Group Permissions

ID Group ID Action Subject Fields Conditions Inverted Reason
1 1 read ["User"] NULL {"id": "$id"} NULL
2 1 update ["User"] ["mail", "password"] {"id": "$id"} NULL
3 1 read ["UserPermission"] NULL {"userId": "$id"} NULL
4 1 read ["GroupPermission"] NULL {"groupId": { "in": "$groups" } } NULL
5 2 manage ["all"] NULL NULL NULL
6 3 update ["User"] ["mail"] {"id": "$id"} NULL
7 3 read ["Vote"] NULL {"expires": { "gt": "$now" } } NULL

Here are all of our group permissions. Let's go through each one, one by one.

For the Member group:

  • Permission 1 allows members of the Member group to read all properties about themselves.
  • Permission 2 allows members of the Member group to update their own email and password.
  • Permissions 3 and 4 allow members of the Member group to read all of their own permissions and all of the permissions of the groups that they are a member of.

For the Admin group:

  • Permission 5 allows members of the Admin group to perform any action on any subject. No restrictions!

Finally, for the Alumni group:

  • Permission 6 restricts members of the Alumni group from updating their own email.

Now that we have all of our permissions defined, we can talk about what happens it comes time to construct a GlimpseAbility for John. The casl-ability.factory.ts file is responsible for constructing GlimpseAbilities for users. Since CASL enforces permissions based on which permissions were the last to be applied, we define our GlimpseAbility in reverse order, by applying the lowest-priority permissions first. For John, this means his Ability will be constructed in the following order:

  1. Apply permissions from the Member group. He inherits these permissions from the Admin group.
  2. Apply permissions from the Admin group. He is a direct member of this group.
  3. Apply permissions from the Alumni group. He is a direct member of this group. This group is applied last because it has a higher priority.
  4. Apply permissions which are directly assigned to John. A user's own permissions are always applied last.

In the end, John's permissions will allow him to perform any action other than update his own email. This is because the Admin group gave him permission to perform any action, but the Alumni group restricted him from updating his own email. Since the Alumni group has a higher priority than the Admin group, the Alumni group's permission is applied last, and overrides the Admin group's permission.

What if John wasn't a member of the Admin group, but instead just the member group?

  • John can read all properties on his own account.
  • John can update his own password, but not his email.
  • John can read all of his own permissions.
  • John can read all permissions for groups which he is a member of (in this case, Member and Alumni, but not Admin).
  • John can read Votes which have not yet closed.
  • John can read Images and Videos which have "John" in the name.

Edge Cases

What happens in circular group hierarchies?

Circular hierarchies are not allowed, and should be caught and prevented before a Group's parent ID is saved. However, if one is somehow created, the system will detect it and throw an error.

What happens when a user is a member of both a group and a child group?

Group permissions are first and foremost applied based on the group's priority. If a user is a member of both a group and a child group, the group with the higher priority will be applied first. If the groups have the same priority, the parent group will be applied first.

In our above example, if John was a member of both the Admin and Member groups, the order would not change, since they both have the same priority. However, if the Member group had a priority of 5, then the Admin group would be applied first, and then the Member group.

Why?

Internally, the parent group is actually being applied twice. For example, if both groups have a priority of 0, then the order of application would be:

  • Member
  • Member
  • Admin
  • Alumni

If the Member group had a priority of 5, then the order would be:

  • Member
  • Admin
  • Member
  • Alumni

This is implementation-specific, and may change in future versions of Glimpse, however the end result will remain the same.

What happens when a user is a member of multiple groups with the same priority?

Permissions are applied in the order that they are defined in the database. For this reason, it is recommended (although not currently required) that groups be given unique priorities. This is less important if you do not use any inverted permissions. With normal permissions, the application order does not matter, since all permissions are additive. If you use or anticipate potentially ever using inverted permissions, however, having unique priorities could resolve issues with permissions not being applied in the expected order.

Clone this wiki locally