Combining custom annotations for securing methods with Spring Security
Spring security is really powerful library in its current version and I like it much. You can secure your application on method level several years now (this feature was introduced by Spring Security 2 in 4/2008) but we've upgraded from old Acegi Security only recently. When using method access control in larger scale I started to think about security rules encapsulation into standalone annotation definitions. It's something you can live without but in my opinion it could help readibility and maintainability of the code. Let's present some options we have now ...
Problem decomposition - how to organize our rules?
Have no system at all
This is usually the first approach one can take after reading documentation of Spring Security framework and start to use it in own code. All rules are represented by Strings of SpEL as follows:
@PreAuthorize("principal.userObject.administrator")
public void approveOrganization(Organization organization) {
//... content ...
}
@PreAuthorize("principal.userObject.isOwnerOf(#organization.id)
or principal.userObject.administrator")
public void updateOrganization(Organization organization) {
//... content ...
}
@PreAuthorize("(branch.isPartOf(organization) and principal.userObject.isManagerOf(#branch.id))
or principal.userObject.isOwnerOf(#organization.id)
or principal.userObject.administrator")
public void updateOrganizationBranch(Organization organization, Branch branch) {
//... content ...
}
//and more ... example was shortened
After a while you might notice that certain rules keep repeating. You can see these rules repeat in our example class:
principal.userObject.administrator
// means administrator with super rights is logged in
principal.userObject.isOwnerOf(#organization.id)
// means user that is owner of particular organization
//(has super rights related to his organization) is logged in
branch.isPartOf(organization) and principal.userObject.isManagerOf(#branch.id)
// means user that is manager of particular branch
// (has rights for operations connected with his branch) is logged in
As we all know Strings are evil - you can't refactor them safely and they tend to break your code much more often. So after a while you start to ...
Extract rules into constants
So the code starts to look like this:
private static final String ALLOWED_FOR_ADMINISTRATOR = "principal.userObject.administrator";
private static final String ALLOWED_FOR_OWNER = "principal.userObject.isOwnerOf(#organization.id)";
private static final String ALLOWED_FOR_BRANCH_MANAGER = "(branch.isPartOf(organization) and principal.userObject.isManagerOf(#branch.id))";
@PreAuthorize(ALLOWED_FOR_ADMINISTRATOR)
public void approveOrganization(Organization organization) {
//... content ...
}
@PreAuthorize(ALLOWED_FOR_OWNER + " or " + ALLOWED_FOR_ADMINISTRATOR)
public void updateOrganization(Organization organization) {
//... content ...
}
@PreAuthorize(ALLOWED_FOR_OWNER + " or " +
ALLOWED_FOR_ADMINISTRATOR + " or " +
ALLOWED_FOR_BRANCH_MANAGER)
public void updateOrganizationBranch(Organization organization, Branch branch) {
//... content ...
}
//and more
Or you could create central shared repository of security rules to keep them in single place to have some control over them:
public abstract class SecurityRules {
public static final String ALLOWED_FOR_ADMINISTRATOR = "principal.userObject.administrator";
public static final String ALLOWED_FOR_OWNER = "principal.userObject.isOwnerOf(#organization.id)";
public static final String ALLOWED_FOR_BRANCH_MANAGER = "(branch.isPartOf(organization) and
principal.userObject.isManagerOf(#branch.id))";
}
Bad things happen when you'd like to share some rules across different libraries. For example we have a legacy system using its own security solution for accessing backend administration. SuperAdministrators are authenticated by this legacy system and have supervisor rights in most of our simple applications - so when such admin is authenticated all Spring Security rules should be overriden and all methods should become accessible for such user. But how to share this common rule across different customer installations? I can make up only single solution possible with standard Spring Security behaviour:
@PreAuthorize(SharedSecurityRules.ALLOWED_FOR_SUPERVISOR + " or " +
SecurityRules.ALLOWED_FOR_ADMINISTRATOR)
public void approveOrganization(Organization organization) {
... content ...
}
@PreAuthorize(SharedSecurityRules.ALLOWED_FOR_SUPERVISOR + " or " +
SecurityRules.ALLOWED_FOR_OWNER + " or " +
SecurityRules.ALLOWED_FOR_ADMINISTRATOR)
public void updateOrganization(Organization organization) {
... content ...
}
@PreAuthorize(SharedSecurityRules.ALLOWED_FOR_SUPERVISOR + " or " +
SecurityRules.ALLOWED_FOR_OWNER + " or " +
SecurityRules.ALLOWED_FOR_ADMINISTRATOR + " or " +
SecurityRules.ALLOWED_FOR_BRANCH_MANAGER)
public void updateOrganizationBranch(Organization organization, Branch branch) {
... content ...
}
And I don't see this kind of notation much more readable than our first attempt with raw SpEL strings. What could be done with this?
Extract rules into custom annotations
You can extract your rules into custom annotations and Spring would find them. If you look at AnnotationUtils class you would realize that Spring tries to find annotation not only on method itself but also on annotations of this method. So you can have following custom annotation:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuthorize(AllowedForOrganizationOwner.IS_ORGANIZATION_OWNER)
public @interface AllowedForOrganizationOwner {
String IS_ORGANIZATION_OWNER = "principal.userObject.isOwnerOf(#organization.id)";
}
But there is ONE BIG "BUT"
You can't mix multiple annotations together - Spring will find only the first applicable annotation and uses this one. So we get no further. This solution seems unusable at the first glance.
Wouldn't it be great if we could define something like this?:
@AllowedForAdministrator
public class MyManager {
public void approveOrganization(Organization organization) {
... content ...
}
@AllowedForOrganizationOwner
public void updateOrganization(Organization organization) {
... content ...
}
@AllowedForOrganizationOwner
@AllowedForBranchManager
public void updateOrganizationBranch(Organization organization, Branch branch) {
... content ...
}
}
Do you like it? Does it seem more readable for you?
If so read on - there is a way how to achieve this ...
Compositing security annotations
Let's define following composition system:
- if there are multiple security annotation placed on the same place (ie. method or class) they represent disjunction (ie. logical OR) ... but we might have a way how to change composition behavior to logical AND if necessary
- rules placed on class level combine with method rules always by logical OR - if class defined rules say access allowed they should override possible access denied vote from the rules placed on method
Let's have some examples:
@AllowedForAdministrator
public class MyManager {
public void approveOrganization(Organization organization) {
//has no method annotation - class annotation will be used instead
}
@AllowedForOrganizationOwner
public void updateOrganization(Organization organization) {
//method and class annotations are combined with OR relation
}
@AllowedForOrganizationOwner
@AllowedForBranchManager
public void updateOrganizationBranch(Organization organization, Branch branch) {
//method annotations are combined by default with OR relation
//result it then combined with class annotation with OR relation
//results in administrator or organization owner or branch manager
}
@AllowedForAnyone
public Organization getOrganizationById(Integer id) {
//we want to let this method to by called by anyone but if we would
//not annotate it at all class annotation will deny execution to all
//but the administrator - so we need to add following annotation:
//AllowedForAnyone => @PreAuthorize("true")
}
@RulesRelation(BooleanOperation.AND)
@IsAuthenticatedFully
@AllowedForOrganizationOwner
public void removeOrganizationById(Integer id) {
//when we want to change relationship to require all rules to be satisfied
//we have to use special annotation @RulesRelation changing default relationship among rules
//execution of this method requires user to be manually logged in in current session
//and to represent an organization owner, of course administrator could call this
//method too because class annotations are always added with OR relation
}
}
Tweaking the Spring Security
If you integrate Spring Security into your project you'd probably use the shortcut way represented by security namespace. Unfortunatelly interface that needs to be overriden is was not exposed by this namespace and is somehow supported by the most recent version 3.1 somehow (see issue SEC-1383). But I wasn't able to make it running and was forced to initialize method security in the old way as a set of aspects:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<aop:config proxy-target-class="true">
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PreAuthorize *) * *.* (..))"/>
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PreFilter *) * *.* (..))"/>
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PostAuthorize *) * *.* (..))"/>
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PostFilter *) * *.* (..))"/>
</aop:config>
<!-- Configure custom security interceptor -->
<bean id="experimentalMethodSecurityInterceptor"
class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="securityMetadataSource">
<bean class="cz.novoj.spring.security.aop.ExperimentalPrePostAnnotationSecurityMetadataSource">
<constructor-arg>
<bean class="org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory">
<constructor-arg ref="expressionHandler"/>
</bean>
</constructor-arg>
</bean>
</property>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="validateConfigAttributes" value="false"/>
<property name="accessDecisionManager">
<bean class="org.springframework.security.access.vote.AffirmativeBased">
<constructor-arg>
<list>
<bean class="org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter">
<constructor-arg>
<bean class="org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice">
<property name="expressionHandler" ref="expressionHandler"/>
</bean>
</constructor-arg>
</bean>
</list>
</constructor-arg>
</bean>
</property>
</bean>
<bean id="expressionHandler"
class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler"/>
</beans>
Note:you need to declare AspectJ pointcuts as follows if you want to find not only methods annotated directly with certain annotation type but also methods annotated with annotations annotated with such annotation type:
execution(@(@org.springframework.security.access.prepost.PreAuthorize *) * *.* (..))
As you can see - all classes except one are taken from Spring codebase. The single exception is class ExperimentalPrePostAnnotationSecurityMetadataSource that is meant to be used instead of original PrePostAnnotationSecurityMetadataSource that looks up for annotations of @PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter. My experimental version finds all annotations that represents above mentioned security annotations or are annotated with them. It retrieves all SPeL security rules that these annotations contain and combine them into a single one in context initialization time. Principles of the rules combinations are stated in the beginning of the previous chapter.
Source code of all used extension classes are available in form of GIST (implementation details are not so important for this article as the main idea is): https://gist.github.com/2081353
If you find this idea useful - please vote or comment Spring Security issue I've created for this idea: Spring Security issue documenting this idea
Komentáře