MS Authorization - PROCEED-Labs/proceed GitHub Wiki
Authorization is the security process that determines a user's level of access to the application. In technology, we use authorization to give users or services permission to access some data or perform a particular action. It is vital to note the difference here between authentication and authorization. Authentication verifies the user's identity before allowing them access, and authorization determines what they can do once the system has granted them access.
The PROCEED MS uses a Role-Based Access Control system to manage user authorization and determine what actions a user can perform. Roles are bundles of permissions that are granted to users. A user can have multiple roles, and all the permissions of the roles are additively combined. That is, by adding a permission, a user can never do less than before. All of these apply on a per-space basis, meaning they only apply to the space they are part of.
Roles only exist for organizations, personal spaces also use the role system internally since they share the same server endpoints. In personal spaces, the owner (the space's only user) is automatically granted permission to carry out all actions.
The following terms are important to understand the role system in the MS:
-
Resource: A resource is any protected entity in the management system that can be accessed by users. Resources can be either assets or management assets. We differentiate between resource type and resource instance: A resource type is, for example,
Process, and a resource instance of this resource type would be an actual process stored in the database. - Action: An action is a specific operation that can be performed on a resource, e.g., view, update, create, delete.
- Permission: A permission is a tuple consisting of a resource type and a list of actions that specifies which actions a user can perform on resource instances. Optionally, a permission can have conditions that must be met by resource instances for the user to be able to perform the actions.
- Role: A role is a set of permissions. Roles can be assigned to users, who then inherit the role's permissions. Roles can have expiration dates, after which all permissions are revoked.
While the concept of permissions may seem straightforward, their implementation is more nuanced than simply following the tuples stored within roles. When creating an ability (see CASL section), the permissions are transformed into new tuples with more complex conditions. This transformation is handled in src/management-system-v2/lib/authorization/caslRules.ts.
The MS currently supports the following resources (defined in src/management-system-v2/lib/ability/caslAbility.ts):
ProcessProjectTemplateTaskMachineExecutionRoleUserSettingRoleMappingEnvironmentFolder
Allrepresents all resources together. This is useful to grant full access to every resource in PROCEED.
The MS resources were chosen to enable the system to use only CRUD actions (defined in src/management-system-v2/lib/ability/caslAbility.ts):
noneviewupdatecreatedeletemanageadmin
manageis an alias that representsupdate,create, anddelete(it is not stored as a combination of these as you'll see in the next section).
adminis an action that, when allowed, permits the user to carry out any action. In some places in the MS,adminis explicitly checked, and it is not sufficient to have permission to carry out all other actions.
System admins are automatically granted every permission for every environment.
We store permissions as an object where the keys are resource types and the values are bit arrays representing a list of actions.
"permissions": {
"Role": 16,
"User": 3,
"Process": 49
}The following table represents the power of two values for each action:
| Action (Verb) | Value |
|---|---|
none |
0 |
view |
1 |
update |
2 |
create |
4 |
delete |
8 |
manage |
16 |
admin |
9007199254740991 |
If multiple permissions are assigned to one resource, they are added. For example, if you want to assign view (1) + manage (16) permissions to a resource, it is calculated as view (1) + manage (16) = 17.
adminis equivalent to admin rights on a resource. It is represented by the highest available number, which consists of only ones (11111...).
When a space in PROCEED is created, the following roles and respective permissions are generated:
| Role | Process | Project | Template | Task | Machine | Execution | Role | User | Setting | EnvConfig | All |
|---|---|---|---|---|---|---|---|---|---|---|---|
@admin |
x | x | x | x | x | x | x | x | x | x | admin |
@everyone |
x | x | x | x | x | x | x | x | x | x | x |
@guest |
x | x | x | x | x | x | x | x | x | x | x |
This role is assigned to the user who created the organization (type of space). This role has all permissions for all assets in the organization. Only users with the @admin role can add new users to this role.
The @everyone role initially has no configuration. This role applies to every member of an organization and is not removable.
The @guest role initially has no configuration. It applies to every user who is not a part of the environment. Through this role, resources can be made public to any authenticated user in the MS.
The MS uses CASL for authorization. It bundles all of a user's roles into an Ability. These are used in both the frontend and backend, ensuring that both can determine the actions a user can perform. This is also important in the frontend to adapt the UI to a user's permissions.
An ability is built with a list of rules that depend on 4 parameters (last 3 are optional):
-
User Action
Describes what the user can actually do in the app. User action is a word (usually a verb) that depends on the business logic (e.g.,
prolong,read). -
Subject
The subject or subject type that you want to check the user action on. In our case, it is either a Resource type or Resource instance.
-
Fields
Can be used to restrict a user action only to matched subject's fields (e.g., to allow users to update the
namefield of aProcessand disallow updatingbpmn). -
Conditions
Criteria that determine, based on a Subject (Resource instance), whether an action is allowed (e.g., to allow users to only delete their own Processes).
Abilities are queried with actions and subjects; optionally, fields can also be used. If just a Subject type (Resource type) is used, it will return true if there exists at least one rule that would eventually allow a user to carry out the action.
const ability = // ... here we would actually get an ability
if(ability.can('view', 'Process')){
// ...
}To check objects, the helper function toCaslResource is needed. This adds a field to the object so that CASL knows the resource type of the object.
import { toCaslResource } from "@/lib/ability/caslAbility";
const process = await getProcess();
if (ability.can("update", toCaslResource("Process", process))) {
// ...
}To use abilities in backend code, you need to use getCurrentEnvironment. It checks whether the user is a part of the space and returns their ability if they are. It also returns an activeEnvironment object which contains the parsed spaceId, as this value may have been URI encoded.
"use server";
import { getCurrentEnvironment } from "@/components/auth";
import { toCaslResource } from "@/lib/ability/caslAbility";
// This does not take error handling into account !!
export async function getProcess(spaceId: string, processId: string) {
const { ability, activeEnvironment } = getCurrentEnvironment(spaceId);
const process = await getProcessFromDb(activeEnvironment.spaceId, processId);
if (!ability.can("view", toCaslResource("Process", process))) {
return undefined;
}
return process;
}Tip
getCurrentEnvironment is cached on a per request basis, so you don't have to worry about calling it throughout multiple functions within the same request.
In components that run on the client, you can use the useAbilityStore hook to access the current user's ability. For simpler checks, you can use the AuthCan component, which shows its children only if the user is allowed to perform certain actions on a resource type.
"use client";
import { useAbilityStore } from "@/lib/abilityStore";
import { AuthCan } from "@/components/auth-can";
import { toCaslResource } from "@/lib/ability/caslAbility";
// This does not take error handling into account !!
export async function ProcessPage({ process }: { process: Process }) {
const ability = useAbilityStore((store) => store.ability);
return (
<div>
<AuthCan create Process>
<button>Create new Process</button>
</AuthCan>
{ability.can("update", toCaslResource("Process", process)) && (
<ProcessModeler process={process} />
)}
</div>
);
}Tip
getCurrentEnvironment is cached on a per request basis, so you don't have to worry about calling it throughout multiple functions within the same request.
Note
The following sections provide technical details about how abilities are created and structured internally. This information is not necessary for using abilities in the MS.
The MS backend builds a list of rules that are used to create an ability on the backend and sent to the client so it also has the same ability. For this reason, rules need to be serializable.
Because of the serialization constraint, the conditions used in the MS (implemented in src/management-system-v2/lib/ability/caslAbility.ts) are complex. They use CASL's ConditionsMatcher and follow this structure:
const rule = {
subject: "Process",
action: ["view"],
conditions: {
conditions: {
// Our custom conditions go here
name: {
$in: ["Everyone's process", "free for all"],
},
},
},
};Inside conditions.conditions, a dot-notation path is set, which is used to get a value from the resource instance. Inside the object, a condition value is given to compare the two values according to the condition's logic. Paths can use a wildcard operator *, which makes conditions apply to all children of these paths. The wildcardOperator determines how the resulting values are merged.
Note
Note that these conditions can only be checked when a Resource instance is passed.
This is the rough type of the conditions object:
type ConditionsObject = {
conditions: {
[path: string]: {
[C in ConditionOperator]: Condition;
};
};
wildcardOperator?: "or" | "and";
conditionsOperator?: "or" | "and";
pathNotFound?: boolean;
};| Field | Default | Description |
|---|---|---|
conditions |
The conditions that are applied for a Resource instance | |
wildcardOperator |
"and" |
How to combine results when a wildcard * is used in a path. "and" requires all matched values to satisfy the condition. "or" allows the action when at least one matched value satisfies the condition. |
conditionsOperator |
"and" |
How to combine conditions. "and" only allows the action when all conditions are true. "or" allows the action when at least one condition is true. |
pathNotFound |
false |
What to do when the path in a condition is not found. With false, it is treated as if the condition check returned false. With true, it is treated as if the condition check returned true. |
The following table shows the existing conditions. When we talk about value in condition, we mean the value that was set when defining the condition. When we talk about input value, we talk about the value that is inside a resource instance when the ability is queried.
| Condition | Description |
|---|---|
$in |
Checks if the input value exists in the array provided as the condition value |
$eq |
Checks strict equality (===) between the condition value and input value |
$neq |
Checks strict inequality (!==) between the condition value and input value |
$eq_string_case_insensitive |
Compares two strings for equality ignoring case (both converted to lowercase) |
$gte |
Checks if the input value is greater than or equal to the condition value (for numbers) |
$expired_property |
Checks if a date property from the resource instance is expired (null or before current date) |
$not_expired_property |
Checks if a date property from the resource instance is not expired (null or after current date) |
$expired_value |
Checks if the date in the condition is expired (null or before current date) |
$not_expired_value |
Checks if the date in the condition is not expired (null or after current date) |
$property_eq |
Compares the input value with another property from the resource. Here the condition value is a dot-notation path that leads to another value in the resource instance. |
$property_has_to_be_child_of |
This only applies to Processes and Folders. Checks if the resource is a descendant of a specific folder by traversing up the folder tree |
$property_has_to_be_parent_of |
This only applies to Folders. Checks if the folder resource is an ancestor of a specific folder by traversing up the tree from the condition value |