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