Friday, August 17, 2012

Writing your spring security expression language annotation - PART 3

In the last part of tutorial, I will discuss how to override the behaviour of defualt spring security method expression. You may wonder why I need to override the default behaviour of these methods. The reason behind is that, in recent development project, we are reviewing the developer's code and we hope to maintain a standard coding practice. We find that the default method expression is too flexible. In our case, under similar coding scenario, some developers use hasRole() for security checking while other developers using hasPermission() for security checking. In order to keep the maintainability of the program, we thus have an idea to disallow developer to use certain secruity method expression. That's why we have the crazy idea of overriding the default behaviour of these methods. (This may not be a good idea :P. But anyway, we have implement it :D)

In this example, I simply show how to override the default behaviour of hasRole() method. You can not do this by override the hasRole() method of SecurityExpressionRoot directly because most of the method in this class is marked as final. To archive it, we have to create another new expression root class and expression handler.

Step 1: Create your Expression Root class and Evaluation Context Class with your own expression method

In my case, I want to not allow developer to use certain method (e.g. hasRole()). I can do this by simpily not include this method. Or I can simpily throw exception to alert use not to use it.

You can see the following code that I have involved custom behaviour (i.e. simply throw exception) of hasAuthority(), hasAnyAuthority() , hasRole(), hasAnyRole(). You can add more logic to these method to suite your application requirement.

public class RestrictedSecurityExpressionRoot {

  protected final Authentication authentication;
  private AuthenticationTrustResolver trustResolver;
  private RoleHierarchy roleHierarchy;
  private Set<String> roles;
  public final boolean permitAll = true;

  public final boolean denyAll = false;
  private PermissionEvaluator permissionEvaluator;
  public final String read = "read";
  public final String write = "write";
  public final String create = "create";
  public final String delete = "delete";
  public final String admin = "administration";

  public RestrictedSecurityExpressionRoot(Authentication a) {
    if (a == null) {
      throw new IllegalArgumentException("Authentication object cannot be null");
    }
    this.authentication = a;
  }
  
  //Default behaviour changed
  public final boolean hasAuthority(String authority) {
  //or your can do your own application logic
  throw new RuntimeException("Role Security Checking is not allow in this framework", null);
  }

  //Default behaviour changed
  public final boolean hasAnyAuthority(String[] authorities) {
   //or your can do your own application logic
   throw new RuntimeException("Role Security Checking is not allow in this application framework");

  }

  //Default behaviour changed
  public final boolean hasRole(String role) {
  //or your can do your own application logic
   throw new RuntimeException("Role Security Checking is not allow in this application framework");
   
  }

  //Default behaviour changed
  public final boolean hasAnyRole(String[] roles) {
  //or your can do your own application logic
   throw new RuntimeException("Role Security Checking is not allow in this application framework");
  }

  public final Authentication getAuthentication() {
    return this.authentication;
  }

  public final boolean permitAll() {
    return true;
  }

  public final boolean denyAll() {
    return false;
  }

  public final boolean isAnonymous() {
    return this.trustResolver.isAnonymous(this.authentication);
  }

  public final boolean isAuthenticated() {
    return (!(isAnonymous()));
  }

  public final boolean isRememberMe() {
    return this.trustResolver.isRememberMe(this.authentication);
  }

  public final boolean isFullyAuthenticated() {
    return ((!(this.trustResolver.isAnonymous(this.authentication))) && (!(this.trustResolver.isRememberMe(this.authentication))));
  }

  public Object getPrincipal() {
    return this.authentication.getPrincipal();
  }

  public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
    this.trustResolver = trustResolver;
  }

  public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
    this.roleHierarchy = roleHierarchy;
  }

  private Set<String> getAuthoritySet() {
    if (this.roles == null) {
      this.roles = new HashSet();
      Collection userAuthorities = this.authentication.getAuthorities();

      if (this.roleHierarchy != null) {
        userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
      }

      this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
    }

    return this.roles;
  }

  public boolean hasPermission(Object target, Object permission) {
    return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
  }

  public boolean hasPermission(Object targetId, String targetType, Object permission) {
    return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission);
  }

  public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
    this.permissionEvaluator = permissionEvaluator;
  }
 
  private Object filterObject;
  private Object returnObject;
  private Object target;

  public void setFilterObject(Object filterObject) {
    this.filterObject = filterObject;
  }

  public Object getFilterObject() {
    return this.filterObject;
  }

  public void setReturnObject(Object returnObject) {
    this.returnObject = returnObject;
  }

  public Object getReturnObject() {
    return this.returnObject;
  }

  void setThis(Object target) {
    this.target = target;
  }

  public Object getThis() {
    return this.target;
  } 
}


public class RestrictedMethodSecurityEvaluationContext extends StandardEvaluationContext {
 
    private static final Log logger = LogFactory.getLog(RestrictedMethodSecurityEvaluationContext.class);
    private ParameterNameDiscoverer parameterNameDiscoverer;
    private final MethodInvocation mi;
    private boolean argumentsAdded;
  
    public RestrictedMethodSecurityEvaluationContext(Authentication user, MethodInvocation mi)
    {
      this(user, mi, new LocalVariableTableParameterNameDiscoverer());
    }
  
    public RestrictedMethodSecurityEvaluationContext(Authentication user, MethodInvocation mi, ParameterNameDiscoverer parameterNameDiscoverer)
    {
      this.mi = mi;
      this.parameterNameDiscoverer = parameterNameDiscoverer;
    }
  
    public Object lookupVariable(String name)
    {
      Object variable = super.lookupVariable(name);
  
      if (variable != null) {
        return variable;
      }
  
      if (!(this.argumentsAdded)) {
        addArgumentsAsVariables();
        this.argumentsAdded = true;
      }
  
      variable = super.lookupVariable(name);
  
      if (variable != null) {
        return variable;
      }
  
      return null;
    }
  
    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
      this.parameterNameDiscoverer = parameterNameDiscoverer;
    }
  
    private void addArgumentsAsVariables() {
      Object[] args = this.mi.getArguments();
  
      if (args.length == 0) {
        return;
      }
  
      Object targetObject = this.mi.getThis();
      Class targetClass = AopProxyUtils.ultimateTargetClass(targetObject);
  
      if (targetClass == null)
      {
        targetClass = targetObject.getClass();
      }
  
      Method method = AopUtils.getMostSpecificMethod(this.mi.getMethod(), targetClass);
      String[] paramNames = this.parameterNameDiscoverer.getParameterNames(method);
  
      if (paramNames == null) {
        logger.warn("Unable to resolve method parameter names for method: " + method + ". Debug symbol information is required if you are using parameter names in expressions.");
  
        return;
      }
  
      for (int i = 0; i < args.length; ++i)
        super.setVariable(paramNames[i], args[i]);
   }
}


Step 2: Create your Expression Handler class which implements SecurityExpressionHandler and ApplicationContextAware.

In this class, you have to implements your createEvaluationContext(). This is the key method to create your newly defined security expression root class (i.e. RestrictedSecurityExpressionRoot) and evaluation context class (i.e. RestrictedMethodSecurityEvaluationContext).

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.access.PermissionCacheOptimizer;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.DenyAllPermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
/**
 * 
 * Extended expression-handler facade which create RestrictedSecurityExpressionRoot instead of SecurityExpressionRoot
 * More restriction will be placed the use of Security Expression.
 * 
 * @author 
 */
public class RestrictedMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler, SecurityExpressionHandler<MethodInvocation>, ApplicationContextAware {
 
 
 private final AuthenticationTrustResolver trustResolver;
 private final ExpressionParser expressionParser;
 private BeanResolver br;
 private RoleHierarchy roleHierarchy;
 private PermissionEvaluator permissionEvaluator;
 
 public RestrictedMethodSecurityExpressionHandler()
 {
   this.trustResolver = new AuthenticationTrustResolverImpl();
   this.expressionParser = new SpelExpressionParser();
 
   this.permissionEvaluator = new DenyAllPermissionEvaluator(); }
 
 public final ExpressionParser getExpressionParser() {
   return this.expressionParser;
 }
 
 public final EvaluationContext createEvaluationContext(Authentication authentication, MethodInvocation invocation)
 {
   RestrictedSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, invocation);
   root.setTrustResolver(this.trustResolver);
   root.setRoleHierarchy(this.roleHierarchy);
   StandardEvaluationContext ctx = createEvaluationContextInternal(authentication, invocation);
   ctx.setBeanResolver(this.br);
   ctx.setRootObject(root);
 
   return ctx;
 }
 
 
 protected RestrictedSecurityExpressionRoot createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation)
 {
     RestrictedSecurityExpressionRoot root = new RestrictedSecurityExpressionRoot(authentication);
     root.setThis(invocation.getThis());
     root.setPermissionEvaluator(getPermissionEvaluator());
     return root;
   }
 public void setRoleHierarchy(RoleHierarchy roleHierarchy)
 {
   this.roleHierarchy = roleHierarchy;
 }
 
 protected PermissionEvaluator getPermissionEvaluator() {
   return this.permissionEvaluator;
 }
 
 public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
   this.permissionEvaluator = permissionEvaluator;
 }
 
 public void setApplicationContext(ApplicationContext applicationContext) {
   this.br = new BeanFactoryResolver(applicationContext);
 }
 
 protected final Log logger = LogFactory.getLog(super.getClass());

 private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
 private PermissionCacheOptimizer permissionCacheOptimizer = null;

 public StandardEvaluationContext createEvaluationContextInternal(Authentication auth, MethodInvocation mi)
 {
   return new RestrictedMethodSecurityEvaluationContext(auth, mi, this.parameterNameDiscoverer);
 }


 public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx)
 {
  RestrictedSecurityExpressionRoot rootObject = (RestrictedSecurityExpressionRoot)ctx.getRootObject().getValue();
   boolean debug = this.logger.isDebugEnabled();

   if (debug) {
     this.logger.debug("Filtering with expression: " + filterExpression.getExpressionString());
   }

   if (filterTarget instanceof Collection) {
     Collection collection = (Collection)filterTarget;
     List retainList = new ArrayList(collection.size());

     if (debug) {
       this.logger.debug("Filtering collection with " + collection.size() + " elements");
     }

     if (this.permissionCacheOptimizer != null) {
       this.permissionCacheOptimizer.cachePermissionsFor(rootObject.getAuthentication(), collection);
     }

     for (Iterator i$ = ((Collection)filterTarget).iterator(); i$.hasNext(); ) { Object filterObject = i$.next();
       rootObject.setFilterObject(filterObject);

       if (ExpressionUtils.evaluateAsBoolean(filterExpression, ctx)) {
         retainList.add(filterObject);
       }
     }

     if (debug) {
       this.logger.debug("Retaining elements: " + retainList);
     }

     collection.clear();
     collection.addAll(retainList);

     return filterTarget;
   }

   if (filterTarget.getClass().isArray()) {
     Object[] array = (Object[])(Object[])filterTarget;
     List retainList = new ArrayList(array.length);

     if (debug) {
       this.logger.debug("Filtering array with " + array.length + " elements");
     }

     if (this.permissionCacheOptimizer != null) {
       this.permissionCacheOptimizer.cachePermissionsFor(rootObject.getAuthentication(), Arrays.asList(array));
     }

     for (Object o : array) {
       rootObject.setFilterObject(o);

       if (ExpressionUtils.evaluateAsBoolean(filterExpression, ctx)) {
         retainList.add(o);
       }
     }

     if (debug) {
       this.logger.debug("Retaining elements: " + retainList);
     }

     Object[] filtered = (Object[])(Object[])Array.newInstance(filterTarget.getClass().getComponentType(), retainList.size());

     for (int i = 0; i < retainList.size(); ++i) {
       filtered[i] = retainList.get(i);
     }

     return filtered;
   }

   throw new IllegalArgumentException("Filter target must be a collection or array type, but was " + filterTarget);
 }

 public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
   this.parameterNameDiscoverer = parameterNameDiscoverer;
 }

 public void setPermissionCacheOptimizer(PermissionCacheOptimizer permissionCacheOptimizer) {
   this.permissionCacheOptimizer = permissionCacheOptimizer;
 }

 public void setReturnObject(Object returnObject, EvaluationContext ctx) {
   ((RestrictedSecurityExpressionRoot)ctx.getRootObject().getValue()).setReturnObject(returnObject);
 }
 
}


Step 3: Add your custom expression handler to your configuration file
The last step is to add your custom security expression handler to the XML file. The permissionEvaluator is created in previous post. You could see the souce code in this link "PART 1"

Now, when developer use the annotation @PreAuthorize("hasRole('XXX')"), it will throws exception. This is only an simple example. You could apply the same idea here and build your own custom logic inside the spring's default method (hasRole(), hasAuthority() , etc.) to suit your application requirement and logic.


     
  

2 comments:

Shaun McCready said...

By chance, do you know if there is a way to access the roles listed in the
@PreAuthorize("hasRole('XXX') or hasRole('YYY')")
when you throw the exception. Reason why is after I catch the exception I want to tell the user, "You need either roles: XXX or YYY in order to access this"

Yogesh Kumar said...

Hi,

I have written all three class in my project and also mention xml config which is written in post 1. It is throwing ClassNoFoundException org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandlerr