In my previous post I looked at adapting Spring Security to work with Permissions to avoid hard-coding Roles into the security configuration. In this post I want to move beyond generic roles and permissions to look at how we can grant or deny access to specific objects within our application.
One solution is to use an Access Control List or ACL, but this can be overkill in some scenarios. So I want to show two methods of introducing domain object security that do not rely on ACLs.
Source code and tests are available on GitHub.
Domain object security using a Custom PermissionEvaluator
One approach in Spring Security is to provide a custom PermissionEvaluator, which:
… is intended to bridge between the expression system and Spring Security’s ACL system, allowing you to specify authorization constraints on domain objects, based on abstract permissions. It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required.
~ Spring 3.2.6 Docs - my emphasis
The PermissionEvaluator has the following interface:
1
2
3
4
5
6
public interface PermissionEvaluator extends AopInfrastructureBean {
boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission);
}
The idea is that you use the first method if you have the object itself, and the second method if you just have the ID. The methods correspond to the following expressions within our service methods (the Authentication parameter is automatically added by Spring):
1
2
3
4
5
@PreAuthorize("hasPermission(#member, 'isOwner')")
void createMember(Member member);
@PreAuthorize("hasPermission(#id, 'Member', 'isOwner')")
void deleteMember(Long id);
By default, Spring uses a DenyAllPermissionEvaluator, which simply denies all requests from hasPermission. We will replace this with our own implementation.
Consider the common scenario where we want to restrict operations on a Member object to just the owner of that object. Here is a simple implementation of PermissionEvaluator that does this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MemberPermissionEvaluator implements PermissionEvaluator {
@Autowired
private MemberRepository memberRepository;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission) {
boolean hasPermission = false;
if(targetDomainObject != null && targetDomainObject instanceof Member) {
Member member = (Member)targetDomainObject;
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
hasPermission = userDetails.getUsername().equals(member.getUsername());
}
return hasPermission;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
boolean hasPermission = false;
if(targetId != null && targetId instanceof Long && "Member".equals(targetType)) {
Long id = (Long)targetId;
Member member = memberRepository.findOne(id);
if(member != null) {
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
hasPermission = userDetails.getUsername().equals(member.getUsername());
}
}
return hasPermission;
}
}
Now when we run our tests, a logged in user can only perform operations on its own associated Member.
Since Spring Security uses a single PermissionEvaluator, should we require similar restrictions placed on other objects (which is likely), then we would have to write a better PermissionEvaluator that delegated to other PermissionEvaluators[1]. So while this approach provides a flexible hook into Spring Security, there is some boilerplate code to write if we want to deal with anything except the simplest requirements. At the other extreme, the flexibility of the PermissionEvaluator interface can sometimes make it appear over-general for simple domain object needs – like the example given here – leaving us having to work with unecessarily complex signitures and generic Objects.
An alternative is to use Spring’s Expression Language.
Domain Object security using Spring Expression Language
Because the string passed into @PreAuthorize and @PostAuthorize annotations is parsed as a Spring Expression Langauge (SpEL) expression, we can write something like this:
1
2
@PreAuthorize("#member.username == principal.username")
void updateMember(Member member);
Where #member references an argument in the method signature. That is to say, it refers to the object that is passed in to the method when the security check is performed. And principal is the currently logged-in user. The available variables are derived from SecurityExpressionRoot, with others added depending on the context. In our example we also have access to the public fields and methods from MethodSecurityExpressionRoot. This latter class provides a getReturnObject() method, which allows us to write:
1
2
@PostAuthorize("returnObject!=null and returnObject.username.equals(principal.username)")
Member readMember();
This is an extremely powerful feature. We can use it to construct increasingly detailed security constraints for our application.
Since our security expressions could get complex, we won’t do that. Instead, lets encapsulate this into a component:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component("memberPermissions")
public class MemberPermissions {
public boolean isOwner(Member targetDomainObject) {
boolean hasPermission = false;
if(targetDomainObject != null) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
hasPermission = userDetails.getUsername().equals(targetDomainObject.getUsername());
}
return hasPermission;
}
}
The Explression Language allows accessing any registered bean by preceding it with an @ symbol[2]. Since MemberPermissions is a Component we can access it from our method security annotations using its name, memberPermissions. From there we can call one of its methods, passing a Member object as a parameter:
1
2
@PreAuthorize("@memberPermissions.isOwner(#member)")
void updateMember(Member member);
Similarly, we can use the getReturnObject() method from MethodSecurityExpressionRoot to authorise based on a methods return value:
1
2
@PostAuthorize("@memberPermissions.isOwner(returnObject)")
Member readMember();
If we want to add more permissions, we just add other methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component("memberPermissions")
public class MemberPermissions {
public boolean isOwner(Member targetDomainObject) {
boolean hasPermission = false;
if(targetDomainObject != null) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
hasPermission = userDetails.getUsername().equals(targetDomainObject.getUsername());
}
return hasPermission;
}
public boolean isCollaborator(Member targetDomainObject) {
// ... code here ...
}
}
And if we want to work with other domain objects we can give that type its own ***Permissions class.
Its hard not to like this approach. Apart from the @Component annotation, there is no other configuration necessary. We can use MemberPermissions within our security annotations straight away. This makes adding new domain object security rules straightforward. We can also easily add other methods (like isEditor(), isReviewer()) just by adding another method to MemberPermissions.
There is a great deal of flexibility here too. For example, if we so choose we can pass in in a value for the current principalin the authorisation annotations:
1
2
@PreAuthorize("@memberPermissions.isOwner(#member, principal)")
void updateMember(Member member);
And so simplify our MemberPermissions code (note the automatic casting to UserDetails):
1
2
3
4
5
6
7
8
public boolean isOwner(Member targetDomainObject, UserDetails principal) {
boolean hasPermission = false;
if(targetDomainObject != null) {
hasPermission = principal.getUsername().equals(targetDomainObject.getUsername());
}
return hasPermission;
}
Other nice things about this approach when compared to using a custom PermissionEvaluator include the lack of superfluous code, the cleaner design (we can choose to implement it as we wish), the use of Member explicitly rather than generic Objects and the corresponding automatic casting.
And finally …
Be aware that these expressions have changed a little over time, for example in earlier versions of Spring Security you would write the expression without the @ symbol (see Note 2 below).
The source is available on GitHub. It contains all the examples here including some basic tests.
Notes
-
Examples of how to do this can be seen here, here, and here.
-
This feature is not available in Spring Security 3.0 (See here for discussion and a workaround). It is available 3.1 but you must leave out the
@symbol. It works as shown in 3.2.