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.
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.
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);
}
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.
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.
Note
|
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, which we will do in PermissionEvaluatorConfiguration
.
At present, we could only annotate PermissionEvaluatorConfiguration
with @EnableGlobalMethodSecurity(prePostEnabled = true)
to protect our methods annotated with @PreAuthorize
.
That works and we would be done, but there’s a slight catch related to more recent developments around @EnableMethodSecurity
.
The documentation lists some of the benefits of using EnableMethodSecurity
over EnableGlobalMethodSecurity
.
We need to add the following configuration to wire up our custom permission evaluator.
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class PermissionEvaluatorConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorizeAuthorizationMethodInterceptor(CustomPermissionEvaluator customPermissionEvaluator) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(customPermissionEvaluator);
PreAuthorizeAuthorizationManager authorizationManager = new PreAuthorizeAuthorizationManager();
authorizationManager.setExpressionHandler(expressionHandler);
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(authorizationManager);
}
}
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
.
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.