PalmyraPermissionEvaluator#
com.palmyralabs.palmyra.ext.aclmgmt.service.PalmyraPermissionEvaluator
Overview#
Spring Security PermissionEvaluator implementation. Annotated @Component — once palmyra-dbacl-mgmt is on the classpath, it registers automatically and answers every hasPermission(authentication, target, permission) expression in your @PreAuthorize / @PostAuthorize annotations and security-method expressions.
Dispatch is strategy-based: each AclPermissionChecker bean in the context gets asked, via supports(...), whether it handles the target. The first checker that claims the target wins. If no checker claims it, the default PalmyraAclProvider takes over and resolves the permission against the ACL tables.
How handler ACL flows through this evaluator#
Handler-level ACL is declared with the @Permission annotation on a handler component — e.g. @Permission(query = "USER_READ", create = "USER_CREATE", ...). When a request reaches a handler, Palmyra evaluates each value in @Permission through the standard Spring PermissionEvaluator — the same path @PreAuthorize("hasPermission(...)") would take.
That means:
- If
PalmyraPermissionEvaluatoris the registered bean, handler permissions resolve against the ACL tables (optionally short-circuited by any matchingAclPermissionChecker). - If you register your own
PermissionEvaluator(see Using your own PermissionEvaluator below), handler permissions resolve through that — no changes to handler code required. The@Permissionkeys stay the same; only the backing implementation changes.
This is the seam that makes ACL pluggable: @Permission is the declarative contract on the handler, and the chosen PermissionEvaluator is the runtime policy engine.
Dependencies#
Lombok-generated constructor over two final fields:
private final List<? extends AclPermissionChecker> checkers; // all beans of this type
private final PalmyraAclProvider aclProvider;An internal Map<String, AclPermissionChecker> checkerMap is used as a per-targetType cache for the id-based overload, so strategy lookup is O(1) after the first call.
Methods#
| Method | Signature |
|---|---|
hasPermission |
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) |
hasPermission |
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) |
hasPermission(auth, targetDomainObject, permission)#
- Iterate the injected
checkers. The first whosesupports(targetDomainObject)returnstrueanswers the call — itshasPermission(...)result is returned directly. - If no checker claims the target, coerce
permissiontoStringand delegate toaclProvider.hasPermission(authentication.getName(), targetDomainObject, sPermission)— the DB-backed default.
hasPermission(auth, targetId, targetType, permission)#
- Look up
targetTypein the internalcheckerMapcache; if a checker is already bound, call it directly. - Otherwise iterate
checkers, bind the first one whosesupports(targetType)returnstrueinto the cache, and delegate. - If nothing claims the target, return
false(no default ACL-table fallback for this overload).
Using your own PermissionEvaluator#
Note. Palmyra is happy with any Spring-standard
PermissionEvaluator. If you already have a permission layer you trust — a Keycloak/OPA bridge, an in-house RBAC, a remote SaaS authorization service — register your own bean and every@Permission-annotated handler plus every@PreAuthorizeexpression will route through it. Palmyra does not requirePalmyraPermissionEvaluatorto be the chosen implementation; it only ships this evaluator as the default, DB-backed option.
Replacing the evaluator#
Publish a single @Primary bean — Spring’s MethodSecurityExpressionHandler picks it up:
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityExpressionsConfig {
@Bean
@Primary
PermissionEvaluator myPermissionEvaluator() {
return new PermissionEvaluator() {
@Override
public boolean hasPermission(Authentication auth, Object target, Object perm) {
// your logic — OPA call, role lookup, feature flag, whatever
return authorize(auth.getName(), target, (String) perm);
}
@Override
public boolean hasPermission(Authentication auth, Serializable id,
String targetType, Object perm) {
return authorize(auth.getName(), targetType + "#" + id, (String) perm);
}
};
}
@Bean
MethodSecurityExpressionHandler expressionHandler(PermissionEvaluator evaluator) {
var handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(evaluator);
return handler;
}
}Augmenting (not replacing) the default#
If you want the DB-backed flow and a custom rule for a specific domain object, implement AclPermissionChecker, register it as a bean, and restrict its supports(...). PalmyraPermissionEvaluator asks every checker before falling back to PalmyraAclProvider, so your checker will be consulted first:
@Component
public class OrderOwnerChecker implements AclPermissionChecker {
@Override
public boolean supports(Object target) { return target instanceof Order; }
@Override
public boolean supports(String targetType) { return "Order".equals(targetType); }
@Override
public boolean hasPermission(Authentication auth, Object target, Object permission) {
Order order = (Order) target;
return order.getOwner().equals(auth.getName())
|| /* fallback to role check */ false;
}
@Override
public boolean hasPermission(Authentication auth, Serializable id,
String targetType, Object permission) {
return hasPermission(auth, orderRepo.findById((Long) id).orElseThrow(), permission);
}
}Usage from a handler / controller#
Once the evaluator is wired (either the default or your replacement), the call sites are plain Spring method-security expressions:
@PreAuthorize("hasPermission(#id, 'Order', 'READ')")
public Order getOrder(@PathVariable Long id) { ... }
@PreAuthorize("hasPermission(#order, 'WRITE')")
public Order update(@RequestBody Order order) { ... }Palmyra handlers that enforce ACL at the filter level (via the extension’s filter-chain integration) don’t need @PreAuthorize as well — pick the layer that fits your endpoint, don’t double up.