Skip to content

Authorization implementation

The platform implements authorization as explained in the Authorizations model guide.

This guide will explain how the checks are performed in the API and front-end code.

Overview

The various actions a user performs in the platform can be authorized depending on the user's Role Assignments.

For each user, an object representing all its Role Assignments can be created at once, then used for authorization checks whenever needed.

This object is created using the CASL library. This library can be used in the API as well as in the front-end code to represent and access any user's permissions.

Unique authorization check

To check authorization in the same way for both the API and the front-end, some authorization code is shared in the monorepository. The only requirements are to provide the user's CASL object, the permission assignment to check for, and some context on which the action would be performed.

For example, to check if you can delete a device:

import { isAllowed } from "core/shared/models/authorization/context";

const device: Device = ...;

const canDeleteDevice = isAllowed(ability, "Delete.Device", {
      tenant: device.tenant,
      folder: device.folder,
});

ability is the user CASL ability, we will explain next how we can create this object.

"Delete.Device" is an permission assignment. It should match what we have stored in our applicable permission assignments as shown in the authorization guide.

{ tenant: ..., folder: ...} is the authorization context. It indicates on which objects the action will be performed. If there is no folder in our context, we could only reference the current tenant for example.

isAllowed also checks recursively on the tenant and folder parents, since users can inherit access rights from tenant and folder ancestors.

Creating a user CASL ability

The user's CASL ability object is of type Ability.

The API is in charge of creating it, according to the role assignments that the user has in the API's database. This happens in the AuthorizationGuard, right after the user has been authenticated.

The AbilityFactory called by the Guard retrieves a user's role assignments and creates its CASL object. Each role assignment is translated to some CASL rules. For example:

  • Technician role definition:
(Technician, Read, Tenant)
(Technician, Read, Device)
(Technician, Create, Device)
  • Role Assignment definitions for user Bob:
(Tenant having id 61, Technician, Bob)

will apply the following CASL rules for user Bob:

builder.can("Read.Tenant", "Tenant", { id: 61 });
builder.can("Read.Device", "Tenant", { id: 61 });
builder.can("Create.Device", "Tenant", { id: 61 });

builder refers to the CASL Ability builder. You can check out CASL's defining rules documentation for more information.

Using the CASL ability directly

Using the example of the previous section, we can check what Bob is allowed to do. ability refers to the CASL Ability that we built using builder in the previous section.

  • Can Bob read a device on tenant 61?
const tenant = new Tenant();
tenant.id = 61;
ability.can("Read.Device", tenant); // true
  • Can Bob create a device on tenant 75?
const tenant = new Tenant();
tenant.id = 75;
ability.can("Create.Device", tenant); // false

CASL does not only check the properties of the object on which we test if we can perform an action. It also checks that the object's type is correct, as we can see in the following example:

  • Can Bob read a device on folder 61?
const folder = new Folder();
folder.id = 61;
ability.can("Read.Device", folder); // false

It is also possible to find all the property values on which you can perform an action:

const tenantRules = ability.rulesFor("Read.Device", "Tenant");
const allowedTenantIds = tenantRules.filter((rule) => rule.conditions).map((rule) => `${rule.conditions.id}`); // [ 61 ]

For most cases, it is easier to call the isAllowed function as shown above. This function will call the right ability.can depending on the context you provide.

Retrieving the user's CASL object

In the API

Once the CASL ability is created in the AuthorizationGuard, you can access it in any controller method using the @GetAbility() decorator:

@Post("devices")
async create(
    @Body() createDeviceDto: CreateDeviceDtoApi,
    @GetAbility() ability: Ability,
): Promise<FullDeviceDtoApi> {
    ...
    this.deviceAuthorizationService.allowCreate(ability, ...);
    ...
}

In the front-end

The front-end is not able to create the user's CASL ability on its own. Instead, it asks the API to create it. The CASL ability is serialized by the API and deserialized by the front-end as shown in the CASL extra documentation.

You can find the endpoint for retrieving the ability in the API's OpenAPI documentation.

Default permission assignments and entity type names

In the examples, for simplicity we used raw strings such as "Read.Device" for CASL actions and "Tenant" for CASL subjects.

However, the actions and subjects that should exist in the platform by default are represented in the enumerations PermissionAssignments and EntityTypes so as not to use raw strings everywhere in code that checks for authorization. You can find both of them in the shared monorepository code.