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.


     
  

Monday, August 13, 2012

Writing your spring security expression language annotation - PART 2

We are now going into the second part of the tutorial. In this post, it will show you how to add a new custom expression for @PreAuthorize annotation. For example, I will show how to add a adminOnly() expression language to the security expression root.


Step 1: Define your custom security expression root class
You have to first create a new security expression root class. This class should be extended from the abstract class org.springframework.security.access.expression.SecurityExpressionRoot. You can add your custom

This class is similar to org.springframework.security.access.expression.method.MethodSecurityExpressionRoot but with your new custom method added. As an example, I just add a very simple mehod adminOnly() which check if the user has admin role.


public class MyMethodSecurityExpressionRoot extends SecurityExpressionRoot {
 
    private static  Logger logger = LoggerFactory.getLogger(MyMethodSecurityExpressionRoot.class);
 
    private Object filterObject;
    private Object returnObject;
    private Object target;
    
     
    public  boolean adminOnly() {
     logger.debug("haha -- check if this function is used by admin role only");
     return  this.hasAuthority("ADMIN");
    }
    
    public MyMethodSecurityExpressionRoot(Authentication a) {
  super(a);
    }

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

    public Object getFilterObject() {
        return filterObject;
    }

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

    public Object getReturnObject() {
        return returnObject;
    }

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

    public Object getThis() {
        return target;
    }

}


Step 2: Define your custom expression handler class
To add custom security expression method, you cannot use the DefaultMethodSecurityExpressionHandler. You need to define a new expression handler class by extends the DefaultMethodSecurityExpressionHandler.

You have to override the createSecurityExpressionRoot() method to create your custom security expression root class.


public class MyMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler  {

   @Override
   protected SecurityExpressionRoot createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
  MyMethodSecurityExpressionRoot root = new MyMethodSecurityExpressionRoot(authentication);
         root.setThis(invocation.getThis());
         root.setPermissionEvaluator(getPermissionEvaluator());
         return root;
   }
}


Step 3: Register the custom expression handler in XML


<sec:global-method-security pre-post-annotations="enabled">
  <sec:expression-handler ref="expressionHandler"/>
</sec:global-method-security>

<bean id="expressionHandler" class="org.borislam.security.ExtendedMethodSecurityExpressionHandler">
  <property name="permissionEvaluator" ref="permissionEvaluator"/>
</bean>  

<bean id="permissionEvaluator" class="org.borislam.security.BasePermissionEvaluator"/>





Example usage:

@PreAuthorize("adminOnly()")
 public void doSomething() {
  System.out.println("doSomething!!!"); 
 }

Sunday, August 5, 2012

Writing your spring security expression language annotation - PART 1

Spring security expression language is very useful. It helpes to secure your service/web methods with one line of code. It supports @PreAuthorize and @Secured. In the comming three posts, I will talk about how to add custom behaviour to the @PreAuthorize annotation.

Part 1 - Customize "hasPermission()" expression
Part 2 - Add new customize method security expression
Part 3 - Override default behaviour of spring security expression (e.g. hasRole() , permitAll() ...)

In this post, I will discuss how to add custom rule for permission checking in your application. This is somewhat similar to what describe in Sold Craft's post. You can reference it for more details.


Step 1: Add configuration in your spring security xml file.

You should first add the DefaultMethodSecurityExpressionHandler. It will instantiate a default MethodSecurityExpressionRoot which provides you all the default security expression (e.g. isAutghenticated(), isAnonymous() ,etc ) .

Besides, you have to add a permsisionEvaluator for that ExpressionHandler. If you are using spring security ACL, you could use AclPermissionEvaluator. In our case, we would create a BasePermissionEvaluator as our permission evaluator. You will see in step 2 that we would define custom rules in this permission evaluator.


<sec:global-method-security pre-post-annotations="enabled">
  <sec:expression-handler ref="expressionHandler"/>
</sec:global-method-security>

<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandlerr">
    <property name="permissionEvaluator" ref="permissionEvaluator"/>
</bean>  

<bean id="permissionEvaluator" class="org.borislam.security.BasePermissionEvaluator"/>



Step 2: Create your PermissionEvaluator class
You must define a class that implements the org.springframework.security.access.PermissionEvaluator. You have to override the hasPermission() method and define custom rule in this class.

In my example, the user object contains a HashMap which stored the permissions of the user. I will perform checking the permission String against this Hashmap. This HashMap is populated during login by a filter. This part will not be skipped in this example.

For similicity, I just ignore targetDomainObject parameter in my example. By using the targetDomainObject, you can further define security rules on certian domain object of your application.

public class BasePermissionEvaluator implements PermissionEvaluator{
 
 
 @Override
 public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
  boolean hasPermission = false;
  if ( authentication != null &&  permission instanceof String){

   //implement the permission checking of your application here   
   //you can just check if the input permission is within your permission list

   //In my example, the user object contains a HashMap which stored the permission of the user.
   //The HashMap<String, PrivilegeResult> is populated during using login by filter. This will not be shown in this example 

   User user = SecurityUtil.getUserCredential();
   HashMap<String, PrivilegeResult> pMap =user.getPrivilegeMap();
   PrivilegeResult privResult = pMap.get(permission); 
   hasPermission =  privResult.isAllowAccess();

  } else {
   hasPermission =false; 
  }
  return hasPermission;
 }

 @Override
 public boolean hasPermission(Authentication authentication,
   Serializable targetId, String targetType, Object permission) {
    throw new RimtimeException("Id and Class permissions are not supperted by this application");
 }
}
\
Step 3: Example usage
You could simply add your the @PreAuthorize("hasPermission()") to secure your method.

@PreAuthorize("hasPermission(#user, 'allowDoSomething')")
 public String doSomething()
 {
  //do something
  System.out.println("Do something");
 }

In the next part of this series of tutorial, I will further discuss how to add your new custom method to the security expression root.