Spring Security: Custom Permission Evaluator
Often you’ll find access decisions move beyond simplistic ownership or having a certain role, for instance when users share domain objects with other users. In such cases it’s common to separate permission to view an instance from being able to make changes to the same instance. When your access rules are relatively straightforward, Spring Security offers the PermissionEvaluator interface to secure instance access.
This post is part of a series of blog posts on Spring Security; the code can be found on GitHub.
Implementation
We’ll explore a system where users share spreadsheets, with permissions to view/edit stored separately. We’ve explicitly modeled the permission storage as simple as can be; imagine it’s calling out to a system of record elsewhere.
Security expressions
Opening SpreadsheetService
reveals four methods with different arguments, each annotated with @PreAuthorize
.
The expressions passed into @PreAuthorize
refer to method arguments by name, and use the built in hasPermission(…)
.
@PreAuthorize("hasPermission(#spreadsheet, 'READ')")
public void read(Spreadsheet spreadsheet) {
log.info("Reading {}", spreadsheet);
}
@PreAuthorize("hasPermission(#id, 'com.jdriven.model.Spreadsheet', 'READ')")
public void readById(Long id) {
log.info("Reading Spreadsheet id {} ", id);
}
Meta annotations
To help keep your code readable, and prevent duplication you can create your own meta-annotations. These allow you to write your security expression once, and use your own annotations throughout your code base.
PermissionEvaluator
The default SecurityExpressionHandler
delegates hasPermission
invocations to a unique PermissionEvaluator
bean, if configured.
If no PermissionEvaluator
bean is provided, it will fallback to the DenyAllPermissionEvaluator
to prevent method invocations.
So, in order to make our own access decisions we have to implement the PermissionEvaluator
interface and provide a bean instance.
The interface has the following methods:
public interface PermissionEvaluator extends AopInfrastructureBean {
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission);
}
From the interface method arguments it should be clear how these map to security expression arguments.
You can only provide a single PermissionEvaluator bean, so if you want to support several target types, you’ll have to handle that within a single instance.
|
We’ve provided an CustomPermissionEvaluator
implementation to serve as an example.
It first checks the target domain object type, before calling out to the appropriate permission store to check for access.
The access rules here are modeled quite simply, but you can extend this as much as needed for your domain.
Lastly, the @PreAuthorize
annotations need to be activated through @EnableGlobalMethodSecurity(prePostEnabled = true)
, which is done in PermissionEvaluatorConfiguration
.
Tests
Our CustomPermissionEvaluatorIT
tests use mock users Alice and Bob, with a malicious third user Eve, all trying to read and write a single spreadsheet.
You can see access to the spreadsheet abides by the access permissions stored at the start of each test:
-
Alice is able to both READ and WRITE the spreadsheet.
-
Bob is able to READ the spreadsheet, but get’s an
AccessDeniedException
when he tried to WRITE. -
Eve always gets an
AccessDeniedException
.
Alternatives
You can achieve much of the same functionality, by extending AbstractAclVoter
, discussed separately.
For highly customizable access rules you might even adopt Spring Security’s ACL services, as shipped in spring-security-acl-xxx.jar
.
This provides a very powerful method of encoding, storing and retrieving access permissions.