Skip to main content

Permission Security

By default, the ServicePlatform IdentityServer uses classical Microsoft.AspNetCore.Identity user roles for access control. Additionally, an ItemSecurity system is implemented to secure each entity using user roles, parent-entity traversing, Security Anchors, and other techniques to control read and write access.

The Permission Security system introduces another level of security to the ServicePlatform by creating function-based permissions.

Overview

The Permission Security system requires multiple components, which can be overridden using DI registrations of the respective interfaces. Here is a brief overview of the components:

Permission

This is the smallest part of the Permission Security system. A single permission defines specific access to a particular method or function. A permission is represented as a simple string with multiple segments, separated by a :: delimiter.

The permission strings are mostly generated dynamically when accessing methods. They may also include IDs of affected entities when applicable. A user is assigned permission strings (using Permission Groups) that can contain wildcards and/or numeric checks like gte (greater than or equal), lte (less than or equal), and eq (equal).

The requested permission string is matched against the user's assigned permission strings. If no match is found, access is denied.

Examples:

  • checkout::order::finish – default named permission
  • checkout::deliveryaddress::* – permission with wildcard
  • checkout::deliveryaddress::{deliveryAddressId} – permission with variables
  • checkout::order::finish::lte500 – permission with numeric value checks
  • user::userhandling::read – default named permission
  • user::userhandling::manage – default named permission

Permission Group

The next level is the Permission Group (defined as IPermissionGroupModel), which groups fine-grained permissions into a single, higher-level permission. A Permission Group can be used to render a user interface to assign permissions to a user, and it includes localized strings for labels and parameters.

A Group contains the following fields:

PropertyDescription
NameName of the permission group
TypeType of permission group (a string to allow extensibility); base system supports: boolean, grouped-select, and group-with-parameters
KeyProgrammatic key for this permission
GroupKeyKey to group multiple groups (applicable for grouped-select type groups)
ConfigurationDetailed configuration of the Permission Group (extensible base configuration class, currently used for group-with-parameters type groups)

Process Overview

One or multiple permission groups (with or without configurations) are assigned either directly to a user or to a Role entity (e.g., a Company Role, which is then assigned to a user).

At login time, the Permission Groups (loaded in the User Profile model accessed by the IdentityServer) are attached as separate claims labeled "bsp:permission-group." This allows any system with access to claims values to check the permission groups assigned to a user.

Permission-Check Process

  1. When an action or method first checks a permission string, the permission groups are resolved using the registered IPermissionGroupResolver.
  2. With all resolved permission strings, a PermissionTrie (see Trie) is constructed.
  3. The resulting PermissionTrie is then cached using IMemoryCache for the current user session (or a maximum of 20 minutes) and reused for every call.
  4. Finally, PermissionTrie.HasPermission(string permission) is called to check if the current user has permission to access the action or method.

Permission Trie

The PermissionTrie class is used within the Permission Security system to efficiently check if a specific permission string is available to the user. It is implemented as a Trie data structure, which allows for quick lookups of permissions, especially when dealing with complex permissions with segments, wildcards, and conditional checks.

How It Works

When a user’s permissions are loaded, the PermissionTrie is constructed based on the list of permission strings assigned to the user. Each segment of a permission string (separated by ::) represents a level in the Trie. This hierarchical structure enables efficient matching of permissions, especially for permissions with multiple levels or wildcard segments.

The PermissionTrie.HasPermission(string permission) method checks if the user has a specified permission by traversing the Trie, allowing for both exact matches and pattern-based matches.

Supported Patterns

The PermissionTrie supports several types of permission patterns:

  • Exact Match: Permissions like checkout::order::finish match the full, exact path in the Trie.
  • Wildcard Match: The wildcard symbol * in permission strings (e.g., checkout::deliveryaddress::*) allows matching any value at the wildcard position and all subsequent levels.
  • Conditional Match: Conditions like LTE500, GTE300, or EQ100 are parsed and evaluated during permission checks. For example, checkout::order::finish::LTE500 only matches if the numerical segment is less than or equal to 500.

Benefits of Using Permission Trie

  • Efficiency: The Trie structure allows fast lookups by avoiding a full linear scan of permissions.
  • Pattern Flexibility: With support for wildcards and conditional checks, the Trie accommodates complex permission rules efficiently.
  • Caching: The constructed Trie is cached per user session to reduce re-processing and improve performance, with a default cache duration of 20 minutes.

Composition of a permission string

To compose a permission string (either for a PermissionGroup, or for a specific permission to check), you should use the PermissionStringBuilder. There you have the possibilities to add the single segments using a simple fluent-API. Finally the method .Build() creates the final permission string to work with.

Example usage

// Read-Only permission for any UserModel EntityStructureValueModel
var entityStructurePermission = PermissionStringBuilder.Create().EntityStructureSegment<UserModel>().Wildcard().Read().Build();

// Write permission to a specific UserModel EntityStructureValueModel
var specificEntityStructurePermission = PermissionStringBuilder.Create().EntityStructureSegment<UserModel>().EntityId(entityId).Write().Build();

// Custom permission for 'custommodule' / 'customaction'
var customPermission = PermissionStringBuilder.Create().Segment("custommodule").Segment("customaction").Build();

Background Worker for Permission Group Registration

The Permission Security system includes a background worker, RegisterSecurityGroupsWorker, responsible for registering predefined permission groups when the application starts. This worker ensures that all permission groups are loaded and available for permission checking as soon as users begin interacting with the system.

How It Works

The RegisterSecurityGroupsWorker is implemented as a hosted background service that performs the following steps on startup:

  1. Scoped Service Creation: The worker opens a scoped service context to access required dependencies, such as IImpersonateUserManager and IPermissionGroupStore.

  2. System Impersonation: Using IImpersonateUserManager, the worker operates with system-level privileges, allowing it to perform registration actions without requiring user credentials.

  3. Permission Group Registration: The worker iterates through each permission group defined in the StaticPermissionGroupConfiguration (loaded through application configuration or DI), invoking StorePermissionGroup to ensure each group is saved in the IPermissionGroupStore. If a group already exists, it is updated to reflect the latest configuration.

Adding Permission Groups with RegisterPermissionGroup

The RegisterPermissionGroup extension method on IServiceCollection simplifies defining and adding permission groups within the dependency injection setup. This method is commonly used in the Startup or ModuleRegistrar to register permission groups and is compatible with the RegisterSecurityGroupsWorker configuration.

Here’s how permission groups are added using RegisterPermissionGroup:

services.RegisterPermissionGroup(
"administrator",
LocalizedStringHelper.Create("Is Administrator", LanguagesConstants.AllCultures),
options => options.AddPermission(o => o.Segment("*"))
);

Each permission group is defined with the following key elements:

  • Key: A unique identifier for the permission group, such as "administrator".
  • Localized Name: A name to be displayed in the UI, defined as a LocalizedStringModel.
  • Permissions: Fine-grained permissions are added using the AddPermission method, which accepts permissions with segments, wildcards, or parameters.

The options parameter in RegisterPermissionGroup allows for configuring additional permissions or customization for the permission group. These registered groups are then stored by the background worker on startup.

Basic Setup

Here is a basic example with the default implementations:

Add Basic Permission Security System

In the ModuleRegistrar, add this IServiceCollection extension call:

services.AddPermissionSecurity(o => o
.WithEntityStructurePermissionGroupProvider(attributeSystemBuilder)
);

This initializes the Permission System with the default Entity Structure Permission Group Provider (modifiable with WithPermissionGroupProvider<TProvider>()).

Define/Register Permission Groups

The desired Permission Groups can then be defined/registered using the IServiceCollection extension method RegisterPermissionGroup(..):

services.RegisterPermissionGroup(
"administrator",
LocalizedStringHelper.Create("Is Administrator", LanguagesConstants.AllCultures),
options => options.AddPermission(o => o.Segment("*"))
);

Assign Permission Groups to Users

When the given permission groups are registered and available in the system, they must be added to any user-related data structure (either the user itself or a related structure, e.g., a CompanyRole that is attached to a user).

As this task is project-specific, it needs to be implemented individually. Finally, the abstract Buzzle.Module.Abstractions.Business.Models.BaseProfileModel must populate the property string[] PermissionGroups so that the selected permission groups can be added to the user claims during login.

To maintain consistency in the assigned PermissionGroups, the methods AddPermissionGroup and RemovePermissionGroup of the manager Buzzle.ServicePlatform.Infrastructure.Features.PermissionSecurity.Managers.IPermissionManager can be used.

Check Permissions

After the permission groups are created and the user claims contain the permission groups, permissions can be checked using both automatic and manual techniques:

  • EntityStructure ItemSecurity: When adding .EntityStructureSegment<TModel>() segments while defining the permissions of a permission group, these permissions are automatically checked in the PermissionBasedItemSecurityResolver during Entity ItemSecurity checks.

  • Endpoint Configuration: When defining endpoints, the required permission can be specified in the security configuration (.WithSecurityConfiguration(...)) using the method .WithSecurityPermissionCheck(...).

  • Custom Controller Attribute: To check permission strings for a controller or a specific action, you can use the PermissionAuthorizationAttribute either on the controller or the action.

Cross-Module Permission Groups

When a user's permissions need to span multiple modules, the Permission Security system allows for consistent handling by defining cross-module permission groups. While the system is inherently module-based, you can achieve cross-module functionality by defining the same permission group key across required modules. Each module specifies only its relevant, module-specific permissions.

Permission Groups are stored in user claims and automatically merged when passed to each module. This ensures that a user's permissions are validated against the module-defined permissions seamlessly.

Key Rule: Each module must use the same PermissionGroup key to ensure consistency across modules.

Example Configuration

The following table illustrates how to configure a cross-module permission group for a sample use case:

PropertyValues
PermissionGroup key (same for all modules)deliveryconfiguration-read
Business-Module permissionscompany::deliveryaddress::*::read
Order-Module permissionsorder::configuration::deliveryconfiguration::read

In this example:

  • If a user has the deliveryconfiguration-read permission group set, they automatically gain:
    • company::deliveryaddress::*::read permissions in the Business Module.
    • order::configuration::deliveryconfiguration::read permissions in the Order Module.

This functionality is achieved as the PermissionTrie in each module dynamically loads the module-specific groups and their respective configurations.