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
| Approach | Scope | Use 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
@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:
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 Group | Synonyms |
|---|---|
| READ | GET, FIND, READ, FETCH, VIEW, RETRIEVE, LIST, SEARCH |
| CREATE | CREATE, SAVE, ADD, INSERT, REGISTER, POST |
| UPDATE | UPDATE, EDIT, MODIFY, CHANGE, PATCH, PUT |
| DELETE | DELETE, 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().
CompositePermissionEvaluator
Implements Spring Security's PermissionEvaluator interface and routes permission checks to domain-specific evaluators.
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 Signature | Routing 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
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:
- Strip the domain prefix (e.g.,
USER_READ—READ) - Resolve CRUD synonyms for the action verb
- Match against authorities with target type
METHODthat contain both the action synonym and the domain name
CRUD Synonym Groups
| Group | Synonyms |
|---|---|
| Read | GET, FIND, READ, FETCH, VIEW, RETRIEVE, LIST, SEARCH |
| Create | CREATE, SAVE, ADD, INSERT, REGISTER, POST |
| Update | UPDATE, EDIT, MODIFY, CHANGE, PATCH, PUT |
| Delete | DELETE, 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
| Evaluator | Domain | Repository Bean | Permission 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:
ROLE_HIERARCHY(14 chars) —checked firstPERMISSION(10 chars)GROUP(5 chars)USER(4 chars)ROLE(4 chars)
This ordering prevents ROLE_HIERARCHY_READ from being incorrectly matched by the ROLE evaluator.
Usage in SpEL Expressions
// 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:
| Field | Description | Example |
|---|---|---|
| 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.
PolicyExpressionConverter, which strips hasPermission(). Keep hasPermission() for method-security expressions and method-oriented authoring flows, not for URL-policy enforcement.
Examples of Policy Conditions
// 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
- Implement the
DomainPermissionEvaluatorinterface - Define
domain()to return your domain name (e.g.,"DOCUMENT") - Define
repositoryBeanName()to return the Spring bean name of the repository that handles your domain entities - Register the class as a Spring
@Component—CompositePermissionEvaluatorauto-detects allDomainPermissionEvaluatorbeans in the application context
Example Implementation
@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.