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.

Concept

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.

Terminology

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.

Resources and Actions

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):

  • Process
  • Project
  • Template
  • Task
  • Machine
  • Execution
  • Role
  • User
  • Setting
  • RoleMapping
  • Environment
  • Folder

All represents 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):

  • none
  • view
  • update
  • create
  • delete
  • manage
  • admin

manage is an alias that represents update, create, and delete (it is not stored as a combination of these as you'll see in the next section).

admin is an action that, when allowed, permits the user to carry out any action. In some places in the MS, admin is explicitly checked, and it is not sufficient to have permission to carry out all other actions.

System admin

System admins are automatically granted every permission for every environment.

Storage of Permissions for Roles

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.

admin is 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

@admin

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.

@everyone

The @everyone role initially has no configuration. This role applies to every member of an organization and is not removable.

@guest

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.

MS Roles in CASL

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):

  1. 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).

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

  3. Fields

    Can be used to restrict a user action only to matched subject's fields (e.g., to allow users to update the name field of a Process and disallow updating bpmn).

  4. 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).

Basic Usage of an Ability

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))) {
  // ...
}

Abilities in the backend

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.

Abilities in the frontend

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.

Creation of 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
⚠️ **GitHub.com Fallback** ⚠️