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 PalmyraPermissionEvaluator is the registered bean, handler permissions resolve against the ACL tables (optionally short-circuited by any matching AclPermissionChecker).
  • If you register your own PermissionEvaluator (see Using your own PermissionEvaluator below), handler permissions resolve through that — no changes to handler code required. The @Permission keys 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)#

  1. Iterate the injected checkers. The first whose supports(targetDomainObject) returns true answers the call — its hasPermission(...) result is returned directly.
  2. If no checker claims the target, coerce permission to String and delegate to aclProvider.hasPermission(authentication.getName(), targetDomainObject, sPermission) — the DB-backed default.

hasPermission(auth, targetId, targetType, permission)#

  1. Look up targetType in the internal checkerMap cache; if a checker is already bound, call it directly.
  2. Otherwise iterate checkers, bind the first one whose supports(targetType) returns true into the cache, and delegate.
  3. 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 @PreAuthorize expression will route through it. Palmyra does not require PalmyraPermissionEvaluator to 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.