Creating a Custom Regex Spring Security 3.1 Expression

Summary: What classes to extend in order to make a custom spEL expression for validating with Spring Security 3.1

The other day I found that I would like a way to authorize a user based on a wild card or, better yet, a regex expression, something like:

@PreAuthorize(“hasRegexRole(‘.*_ADMIN’)”)

After much looking around I came up with the following.

First we tell Spring to look at our expression handler for method and web authorization.

    <!--
        Enable class- and method-level security via annotations
    -->
    <sec:global-method-security pre-post-annotations="enabled" secured-annotations="enabled">
    	 <sec:expression-handler ref="expressionHandler"/>
    </sec:global-method-security>

    <!--
        WebSecurityExpressHandler for Spring Security JSP tags
    -->
    <bean id="webSecurityExpressionHandler" class="com.bmchild.security.access.expression.CustomWebSecurityExpressionHandler"/>
    
    <!-- Custom Expression Handler -->
    <bean id="expressionHandler" class="com.bmchild.security.access.expression.CustomMethodSecurityExpressionHandler" />

package com.bmchild.security.access.expression;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.core.Authentication;

/**
* @author bchild
*
*/
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

Logger LOGGER = Logger.getLogger(CustomMethodSecurityExpressionHandler.class);

public CustomMethodSecurityExpressionHandler() {
super();
}

/* (non-Javadoc)
* @see org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler#createSecurityExpressionRoot(org.springframework.security.core.Authentication, org.aopalliance.intercept.MethodInvocation)
*/
@Override
protected SecurityExpressionRoot createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
CustomSecurityExpressionRoot root = new CustomSecurityExpressionRoot(authentication);
root.setPermissionEvaluator(getPermissionEvaluator());

return root;
}

}

And we define our custom security root with our new expression method. Also note the hasIpAddress method. This is there to replace the WebSecurityExpressionRoot so that we can use our new expression on JSPs with spring security’s tag.

package com.bmchild.security.access.expression;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.IpAddressMatcher;

/**
*
* Security Expression evaluator for wildcard use
*
* @author bchild
*
*/
public class CustomSecurityExpressionRoot extends SecurityExpressionRoot {

Logger LOGGER = Logger.getLogger(CustomSecurityExpressionRoot.class);

private Set userRoles;
/*
* This is for emulating WebSecurityExpressionRoot
*/
public HttpServletRequest request;

/**
* @param a
*/
public CustomSecurityExpressionRoot(Authentication a) {
super(a);
}

public CustomSecurityExpressionRoot(Authentication a, FilterInvocation fi) {
super(a);
this.request = fi.getRequest();
}

/**
* Checks given roles against the given regex expression
*
* @param regex
* to match agains roles
* @return true if at least 1 authority matches the regex, otherwise false
*/
public boolean hasRegexRole(String regex) {

if (LOGGER.isDebugEnabled()) {
LOGGER.debug(“hasRegexRole: ” + regex);
}

boolean found = false;

Set authorities = getCustomAuthoritySet();

for (String authority : authorities) {

if (authority.matches(regex)) {
found = true;
break;
}

}

if (LOGGER.isDebugEnabled()) {
LOGGER.debug(“hasRegexRole returns ” + found);
}

return found;
}

/**
*
* @see org.springframework.security.web.access.expression.
* WebSecurityExpressionRoot.hasIpAddress(String) Takes a specific IP
* address or a range using the IP/Netmask (e.g. 192.168.1.0/24 or
* 202.24.0.0/14).
*
* @param ipAddress
* the address or range of addresses from which the request must
* come.
* @return true if the IP address of the current request is in the required
* range.
*/
public boolean hasIpAddress(String ipAddress) {
return (new IpAddressMatcher(ipAddress).matches(request));
}

/**
* Note: this does not return hierchacal roles like
* org.springframework.security
* .access.expression.SecurityExpressionRoot.getAuthoritySet()
*
* @return set of authorities
*/
private Set getCustomAuthoritySet() {

if (userRoles == null) {
userRoles = new HashSet();
Collection userAuthorities = authentication.getAuthorities();

userRoles = AuthorityUtils.authorityListToSet(userAuthorities);
}

return userRoles;
}

}

Test!

package com.bmchild.security.access.expression;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Collection;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

/**
* @author bchild
*
*/
@RunWith(MockitoJUnitRunner.class)
public class CustomSecurityExpressionRootTest {

private CustomSecurityExpressionRoot expression;

private Authentication auth;

@Before
public void before() {
auth = mock(Authentication.class);
expression = new CustomSecurityExpressionRoot(auth);
}

/**
* Test method for {@link com.crengland.security.access.expression.CustomSecurityExpressionRoot#hasRegexRole(java.lang.String)}.
*/
@Test
public void testHasRegexRole() {

// Mock
final Collection authorities = new ArrayList();
authorities.add( new SimpleGrantedAuthority(“ROLE_1_ADMIN”) );
authorities.add( new SimpleGrantedAuthority(“ROLE_2_ADMIN”) );
authorities.add( new SimpleGrantedAuthority(“ROLE_3_ADMIN”) );
authorities.add( new SimpleGrantedAuthority(“ROLE_1_NONADMIN”) );

when(auth.getAuthorities()).thenAnswer( new Answer>() {

@Override
public Collection answer(InvocationOnMock invocation) throws Throwable {
return authorities;
}
});

// Test
assertTrue( expression.hasRegexRole(“ROLE_1_ADMIN”) );
assertTrue( expression.hasRegexRole(“ROLE_.+_ADMIN”) );
assertFalse( expression.hasRegexRole(“ROLE__ADMIN”) );
assertTrue( expression.hasRegexRole(“ROLE_[\\d]+_NONADMIN”) );

// Verify
verify(auth).getAuthorities();

}

}

And we create a custom web security expression handler that returns our custom security root. Again this allows us to use our custom expression method in JSPs, or in URLs if you really wanted.

package com.bmchild.security.access.expression;

import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;

/**
* @author bchild
*
*/
public class CustomWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {

@Override
protected SecurityExpressionRoot createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
CustomSecurityExpressionRoot root = new CustomSecurityExpressionRoot(authentication, fi);
root.setPermissionEvaluator(getPermissionEvaluator());
return root;
}
}

Leave a Reply

Your email address will not be published. Required fields are marked *