contexa-iam

Permission Evaluators

Composite permission evaluation system implementing Spring Security's PermissionEvaluator with domain-specific evaluators for fine-grained method-level access control.

Choosing the Right Approach

ApproachScopeUse Case
hasPermission() Object-level Fine-grained CRUD permissions on specific domain objects
hasRole() Global Broad access control based on user roles
@Protectable Method-level Dynamic policy-driven authorization with AI integration

Quick Start

Use hasPermission() in @PreAuthorize annotations or SpEL policy conditions to check fine-grained permissions.

In Annotations

Java
@PreAuthorize("hasPermission(#id, 'USER', 'READ')")
public User getUser(Long id) { ... }

@PreAuthorize("hasPermission(#id, 'ROLE', 'UPDATE')")
public void updateRole(Long id, RoleDto dto) { ... }

In Policy Conditions

When authoring method-oriented conditions in Policy Center or other policy-editing flows, use hasPermission() in stored SpEL condition expressions:

SpEL
hasPermission(#targetId, 'GROUP', 'DELETE')
hasPermission(#targetId, 'USER', 'READ') and #ai.isAllowed()

CRUD Synonym Mapping

Permission evaluators use synonym groups so that equivalent actions are treated as the same permission. The synonym sets come directly from AbstractDomainPermissionEvaluator.resolveCrudSynonyms(...), including the extended read aliases LIST/SEARCH and delete aliases CLEAR/TRUNCATE.

Permission GroupSynonyms
READGET, FIND, READ, FETCH, VIEW, RETRIEVE, LIST, SEARCH
CREATECREATE, SAVE, ADD, INSERT, REGISTER, POST
UPDATEUPDATE, EDIT, MODIFY, CHANGE, PATCH, PUT
DELETEDELETE, REMOVE, DESTROY, DROP, ERASE, PURGE, CLEAR, TRUNCATE

This means hasPermission(#id, 'USER', 'FETCH') is equivalent to hasPermission(#id, 'USER', 'READ').

Architecture

The permission evaluation system follows a composite pattern. CompositePermissionEvaluator is the entry point. The object-form overload routes by permission-string prefix through supportsPermission(), and the three-argument overload routes by targetType through supportsTargetType().

Text
hasPermission(auth, target, 'USER_READ')
SpEL expression triggers permission evaluation
CompositePermissionEvaluator
Routes to domain evaluators sorted by domain length (longest first)
UserPermissionEvaluator
domain: "USER"
RolePermissionEvaluator
domain: "ROLE"
RoleHierarchyPermissionEvaluator
domain: "ROLE_HIERARCHY"
GroupPermissionEvaluator
domain: "GROUP"
Custom DomainPermissionEvaluator beans
Optional user-defined evaluators registered as Spring beans
PermissionTargetPermissionEvaluator
domain: "PERMISSION"

CompositePermissionEvaluator

Implements Spring Security's PermissionEvaluator interface and routes permission checks to domain-specific evaluators.

Java
public class CompositePermissionEvaluator implements PermissionEvaluator {

    // Sorted by domain length (descending) for longest-prefix matching
    // e.g., ROLE_HIERARCHY matches before ROLE
    private final List<DomainPermissionEvaluator> evaluators;

    // Permission-string routing (e.g., "USER_READ" -> UserPermissionEvaluator)
    @Override
    public boolean hasPermission(Authentication authentication,
                                 Object targetDomainObject,
                                 Object permission) { ... }

    // Target-type routing (e.g., targetType="ROLE" -> RolePermissionEvaluator)
    @Override
    public boolean hasPermission(Authentication authentication,
                                 Serializable targetId,
                                 String targetType,
                                 Object permissionAction) { ... }

    // Entity resolution for UI display
    public Object resolveEntity(Serializable targetId, String targetType) { ... }
}

Routing Logic

Method SignatureRouting Strategy
hasPermission(auth, target, permission) Routes to the first evaluator whose supportsPermission() method returns true for the given permission string. The permission is matched against evaluators by domain prefix (e.g., USER_READ routes to the User evaluator).
hasPermission(auth, targetId, targetType, action) Routes to the first evaluator whose supportsTargetType() method returns true for the given target type. Target type matching is case-insensitive.

DomainPermissionEvaluator Interface

Java
public interface DomainPermissionEvaluator {
    boolean supportsTargetType(String targetType);
    boolean supportsPermission(String permission);
    boolean hasPermission(Authentication auth, Object target, Object permission);
    boolean hasPermission(Authentication auth, Serializable targetId,
                          String targetType, Object permission);
    Object resolveEntity(Serializable targetId);
}

AbstractDomainPermissionEvaluator

Base class providing common functionality for all domain evaluators. Implements CRUD synonym resolution and PermissionAuthority-based permission matching.

Permission Matching

Permission strings are matched against the user's PermissionAuthority grants. The matching process:

  1. Strip the domain prefix (e.g., USER_READREAD)
  2. Resolve CRUD synonyms for the action verb
  3. Match against authorities with target type METHOD that contain both the action synonym and the domain name

CRUD Synonym Groups

GroupSynonyms
ReadGET, FIND, READ, FETCH, VIEW, RETRIEVE, LIST, SEARCH
CreateCREATE, SAVE, ADD, INSERT, REGISTER, POST
UpdateUPDATE, EDIT, MODIFY, CHANGE, PATCH, PUT
DeleteDELETE, REMOVE, DESTROY, DROP, ERASE, PURGE, CLEAR, TRUNCATE

For example, hasPermission(auth, null, 'USER_FETCH') will match if the user has any authority containing USER and any of GET, FIND, READ, FETCH, VIEW, RETRIEVE, LIST, or SEARCH.

Entity Resolution

Each evaluator resolves entities via a named repository bean from the ApplicationContext. The resolveEntity() method calls findById() on the resolved repository using reflection.

Domain Evaluators

EvaluatorDomainRepository BeanPermission Prefix
UserPermissionEvaluator USER userRepository USER_*
RolePermissionEvaluator ROLE roleRepository ROLE_*
RoleHierarchyPermissionEvaluator ROLE_HIERARCHY roleHierarchyRepository ROLE_HIERARCHY_*
GroupPermissionEvaluator GROUP groupRepository GROUP_*
PermissionTargetPermissionEvaluator PERMISSION permissionRepository PERMISSION_*

All evaluators extend AbstractDomainPermissionEvaluator and only define their domain(), repositoryBeanName(), and getApplicationContext() methods. The permission check logic is inherited from the abstract base.

Evaluation Order

Evaluators are sorted by domain string length in descending order. This ensures that more specific domains are matched first:

  1. ROLE_HIERARCHY (14 chars) —checked first
  2. PERMISSION (10 chars)
  3. GROUP (5 chars)
  4. USER (4 chars)
  5. ROLE (4 chars)

This ordering prevents ROLE_HIERARCHY_READ from being incorrectly matched by the ROLE evaluator.

Usage in SpEL Expressions

Java
// Permission-string based (routed by prefix)
@PreAuthorize("hasPermission(null, 'USER_READ')")
public List<UserDto> getUsers() { ... }

// Target-type based (routed by targetType parameter)
@PreAuthorize("hasPermission(#id, 'ROLE', 'EDIT')")
public void updateRole(@PathVariable Long id, @RequestBody RoleDto dto) { ... }

// Group-scoped permission
@PreAuthorize("hasPermission(#groupId, 'GROUP', 'DELETE')")
public void deleteGroup(@PathVariable Long groupId) { ... }

Managing Permissions in the Permission Management UI

The current OSS permission maintenance screen is /admin/permissions. It provides the standalone CRUD UI for permission records, while Policy Center links back to the same permission catalog during policy authoring.

Navigating to Permission Management

Go to /admin/permissions from the IAM menu. This screen lists stored permission records with their target types and action types; role assignment is handled separately in the role management screens.

Creating a New Permission

Click "Create Permission" and fill in the following fields:

FieldDescriptionExample
Name A unique identifier for the permission, typically in DOMAIN_ACTION format USER_READ
Description Human-readable explanation of what this permission grants "Allows reading user records"
Target Type The resource scope: URL for endpoint-level or METHOD for method-level permissions METHOD
Action Type The current standalone form exposes READ, WRITE, EXECUTE, DELETE, and ALL as action metadata. READ

The standalone permission form and Policy Center use related but not identical action vocabularies. /admin/permissions stores actionType metadata on the permission record, while Policy Center quick-create and business-policy flows separately use CRUD selections such as READ, WRITE, UPDATE, and DELETE.

Associating Permissions with Roles

After creating a permission, navigate to the role management section to associate it. Open the target role, go to the "Permissions" tab, and add the newly created permission. When a user with that role invokes hasPermission(), the CompositePermissionEvaluator checks whether the user's PermissionAuthority grants include the matching domain and action.

How Permissions Connect to DomainPermissionEvaluator Routing

Permissions associated with the authenticated principal are represented as PermissionAuthority grants. When hasPermission() is called, CompositePermissionEvaluator routes the call to the appropriate DomainPermissionEvaluator based on the domain prefix or target type. The evaluator then checks whether the user's authorities include a matching permission with the correct action synonym group.

Using hasPermission() in Policy Center and stored expressions

When authoring stored expressions, hasPermission() can be combined with role checks, time checks, and #ai.* conditions. In the current OSS UI, Policy Center is the primary screen for that flow.

Important: The current URL-policy runtime path passes stored expressions through PolicyExpressionConverter, which strips hasPermission(). Keep hasPermission() for method-security expressions and method-oriented authoring flows, not for URL-policy enforcement.

Examples of Policy Conditions

SpEL
// Permission check only
hasPermission(#targetId, 'USER', 'READ')

// Combined with AI security expression
hasPermission(#targetId, 'GROUP', 'DELETE') and #ai.isAllowed()

// Combined with role check
hasPermission(#targetId, 'ROLE', 'UPDATE') or hasRole('ADMIN')

// Combined with time-based condition
hasPermission(#targetId, 'PERMISSION', 'READ') and #time.isBusinessHours()

These expressions are entered in the condition field of the policy authoring flow. The OSS UI supports condition entry and condition catalogs, but the documentation should not assume dedicated parameter auto-completion beyond what the current screen actually exposes.

Creating Custom Domain Evaluators

You can extend the permission evaluation system by implementing the DomainPermissionEvaluator interface for your own domain types. Custom evaluators are automatically detected by CompositePermissionEvaluator when registered as Spring beans.

Implementation Steps

  1. Implement the DomainPermissionEvaluator interface
  2. Define domain() to return your domain name (e.g., "DOCUMENT")
  3. Define repositoryBeanName() to return the Spring bean name of the repository that handles your domain entities
  4. Register the class as a Spring @ComponentCompositePermissionEvaluator auto-detects all DomainPermissionEvaluator beans in the application context

Example Implementation

Java
@Component
public class DocumentPermissionEvaluator
        extends AbstractDomainPermissionEvaluator {

    private final ApplicationContext applicationContext;

    public DocumentPermissionEvaluator(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public String domain() {
        return "DOCUMENT";
    }

    @Override
    public String repositoryBeanName() {
        return "documentRepository";
    }

    @Override
    protected ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

With this bean registered, hasPermission(#id, 'DOCUMENT', 'READ') will automatically route to DocumentPermissionEvaluator. The base class AbstractDomainPermissionEvaluator handles CRUD synonym resolution, PermissionAuthority matching, and entity resolution via the named repository bean.

Auto-Detection and Ordering

CompositePermissionEvaluator collects all DomainPermissionEvaluator beans at startup and sorts them by domain string length (longest first). This ensures that a custom evaluator with domain "DOCUMENT_ARCHIVE" is matched before one with domain "DOCUMENT". No additional configuration is required beyond registering the bean.