How to create custom methods for use in spring security expression language annotations

JavaSpringSpring Security

Java Problem Overview


I would like to create a class that adds custom methods for use in spring security expression language for method-based authorization via annotations.

For example, I would like to create a custom method like 'customMethodReturningBoolean' to be used somehow like this:

  @PreAuthorize("customMethodReturningBoolean()")
  public void myMethodToSecure() { 
    // whatever
  }

My question is this. If it is possible, what class should I subclass to create my custom methods, how would I go about configuring it in the spring xml configuration files and come someone give me an example of a custom method used in this way?

Java Solutions


Solution 1 - Java

None of the mentioned techniques will work anymore. It seems as though Spring has gone through great lengths to prevent users from overriding the SecurityExpressionRoot.

EDIT 11/19/14 Setup Spring to use security annotations:

<beans ... xmlns:sec="http://www.springframework.org/schema/security" ... >
...
<sec:global-method-security pre-post-annotations="enabled" />

Create a bean like this:

@Component("mySecurityService")
public class MySecurityService {
    public boolean hasPermission(String key) {
        return true;
    }
}

Then do something like this in your jsp:

<sec:authorize access="@mySecurityService.hasPermission('special')">
    <input type="button" value="Special Button" />
</sec:authorize>

Or annotate a method:

@PreAuthorize("@mySecurityService.hasPermission('special')")
public void doSpecialStuff() { ... }

Additionally, you may use Spring Expression Language in your @PreAuthorize annotations to access the current authentication as well as method arguments.

For example:

@Component("mySecurityService")
public class MySecurityService {
    public boolean hasPermission(Authentication authentication, String foo) { ... }
}

Then update your @PreAuthorize to match the new method signature:

@PreAuthorize("@mySecurityService.hasPermission(authentication, #foo)")
public void doSpecialStuff(String foo) { ... }

Solution 2 - Java

You'll need to subclass two classes.

First, set a new method expression handler

<global-method-security>
  <expression-handler ref="myMethodSecurityExpressionHandler"/>
</global-method-security>

myMethodSecurityExpressionHandler will be a subclass of DefaultMethodSecurityExpressionHandler which overrides createEvaluationContext(), setting a subclass of MethodSecurityExpressionRoot on the MethodSecurityEvaluationContext.

For example:

@Override
public EvaluationContext createEvaluationContext(Authentication auth, MethodInvocation mi) {
    MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(auth, mi, parameterNameDiscoverer);
    MethodSecurityExpressionRoot root = new MyMethodSecurityExpressionRoot(auth);
    root.setTrustResolver(trustResolver);
    root.setPermissionEvaluator(permissionEvaluator);
    root.setRoleHierarchy(roleHierarchy);
    ctx.setRootObject(root);

    return ctx;
}

Solution 3 - Java

Thanks ericacm, but it does not work for a few reasons:

  • The properties of DefaultMethodSecurityExpressionHandler are private (reflection visibility kludges undesirable)
  • At least in my Eclipse, I can't resolve a MethodSecurityEvaluationContext object

The differences are that we call the existing createEvaluationContext method and then add our custom root object. Finally I just returned an StandardEvaluationContext object type since MethodSecurityEvaluationContext would not resolve in the compiler (they are both from the same interface). This is the code that I now have in production.

Make MethodSecurityExpressionHandler use our custom root:

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler  {
	
	// parent constructor
    public CustomMethodSecurityExpressionHandler() {
        super();
    }
	
    /**
     * Custom override to use {@link CustomSecurityExpressionRoot}
     * 
     * Uses a {@link MethodSecurityEvaluationContext} as the <tt>EvaluationContext</tt> implementation and
     * configures it with a {@link MethodSecurityExpressionRoot} instance as the expression root object.
     */
    @Override
    public EvaluationContext createEvaluationContext(Authentication auth, MethodInvocation mi) {
    	// due to private methods, call original method, then override it's root with ours
    	StandardEvaluationContext ctx = (StandardEvaluationContext) super.createEvaluationContext(auth, mi);
    	ctx.setRootObject( new CustomSecurityExpressionRoot(auth) );
        return ctx;
    }
}

This replaces the default root by extending SecurityExpressionRoot. Here I've renamed hasRole to hasEntitlement:

public class CustomSecurityExpressionRoot extends SecurityExpressionRoot  {

	// parent constructor
    public CustomSecurityExpressionRoot(Authentication a) {
        super(a);
    }
	
	/**
	 * Pass through to hasRole preserving Entitlement method naming convention
	 * @param expression
	 * @return boolean
	 */
    public boolean hasEntitlement(String expression) {
        return hasRole(expression);
    }

}

Finally update securityContext.xml (and make sure it's referenced from your applcationContext.xml):

<!-- setup method level security using annotations -->
<security:global-method-security
		jsr250-annotations="disabled"
        secured-annotations="disabled"
        pre-post-annotations="enabled">
    <security:expression-handler ref="expressionHandler"/>
</security:global-method-security>

<!--<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">-->
<bean id="expressionHandler" class="com.yourSite.security.CustomMethodSecurityExpressionHandler" />

Note: the @Secured annotation will not accept this override as it runs through a different validation handler. So, in the above xml I disabled them to prevent later confusion.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPaul D. EdenView Question on Stackoverflow
Solution 1 - JavaJames WatkinsView Answer on Stackoverflow
Solution 2 - JavasourcedelicaView Answer on Stackoverflow
Solution 3 - JavaJoseph LustView Answer on Stackoverflow