Wednesday, January 19, 2011

Spring Security: Simple ACL using Expression-Based Access Control (Part 1)

In this tutorial we will study how to setup a simple Spring MVC 3 application with ACL-based security using Spring Security 3's Expression-Based Access Control. Our application is a simple Bulletin site where different types of users can read and post messages. It is composed of three sections: Admin Posts, Personal Posts, and Public Posts and three types of users: admin, user, and visitor.

Here's a sample screenshot:


This tutorial assumes you're familiar with setting-up a basic intercept-url based Spring Security application, and Spring MVC application. If you need a review, please read my other tutorials first regarding these topics.

What is Expression-Based Access Control?
Spring Security 3.0 introduced the ability to use Spring EL expressions as an authorization mechanism in addition to the simple use of configuration attributes and access-decision voters which have seen before. Expression-based access control is built on the same architecture but allows complicated boolean logic to be encapsulated in a single expression

Source: http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html

What is ACL?
Complex applications often will find the need to define access permissions not simply at a web request or method invocation level. Instead, security decisions need to comprise both who (Authentication), where (MethodInvocation) and what (SomeDomainObject). In other words, authorization decisions also need to consider the actual domain object instance subject of a method invocation.

Source: http://static.springsource.org/spring-security/site/docs/3.0.x/reference/domain-acls.html

An access control list (ACL), with respect to a computer file system, is a list of permissions attached to an object. An ACL specifies which users or system processes are granted access to objects, as well as what operations are allowed on given objects. Each entry in a typical ACL specifies a subject and an operation. For instance, if a file has an ACL that contains (Alice, delete), this would give Alice permission to delete the file.

Source: http://en.wikipedia.org/wiki/Access_control_list

Functional Specs

To understand more about ACL, let's compare it against a simple Spring Security intercept-url based configuration. If we have three sections, Admin Posts, Personal Posts, Visitors Posts, we'll probably pattern our configuration similar to the following:

<security:intercept-url pattern="/krams/admin" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/personal" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/public" access="hasRole('ROLE_VISITOR')"/>
In this setup, here's what we can observe:
  • Only admins can access Admin Posts
  • Only users can access Personal Posts
  • Only visitors can access Visitors Posts
That's good if that's the client's requirements. What if we have the following requirements instead:

An admin has READ and WRITE access to everything, but only READ access to the Personal Posts. See the table below:

Admin
Post TypeViewAddEditDelete
Adminxxxx
Personalx
Publicxxxx

A regular user has READ and WRITE access to Personal Posts and Public Posts but only READ access to Admin Posts. See the table below:

User
Post TypeViewAddEditDelete
Admin



Personalxxxx
Publicxxxx

A visitor can only read Admin and Public Posts but no access of whatsoever in the Personal Posts section. See the table below:

Visitor
Post TypeViewAddEditDelete
Admin



Personal
Publicx



The Difficulty

To do this using intercept-url you will have to do the following:

Admin Posts
<security:intercept-url pattern="/krams/admin/view" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/admin/add" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/admin/edit" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/admin/delete" access="hasRole('ROLE_ADMIN')"/>

Personal Posts
<security:intercept-url pattern="/krams/personal/view" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/personal/add" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/personal/edit" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/personal/delete" access="hasRole('ROLE_USER')"/>

Public Posts
<security:intercept-url pattern="/krams/public/view" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER') or hasRole('ROLE_VISITOR')"/>
<security:intercept-url pattern="/krams/public/add" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/public/edit" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/public/delete" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
This setup works. However there are some problems that we observe:

1. It only works at the Controller level--that is at the URL level.
2. What happens if we have a domain object that doesn't correspond to a particular URL?
3. What if we need to display the same URL that contains our three domain objects together: Admin, Personal, and Public Posts? It's either we get an Access Denied or we see everything.

The Solution

We use ACL. The idea is we put the restriction on the domain object itself. It's similar to the way we access files in the computer. Various users have different READ and WRITE access to certain files. Some have READ access, but some have both READ and WRITE access.

The Application

We'll begin our application by configuring the required Spring Security configuration and the required custom classes. Then in Part 2, we'll build the Spring MVC section.

In Spring Security 3, there's the heavyweight solution of implementing ACL (See Spring Security 3 Reference Chapter 16). And there's also the lightweight solution of implementing ACL through the use of Expression-Based Access Control

To use it, we need to mark our methods with Method Security Expressions (see Chapter 15.3). They are @PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter.

Then we need to use the hasPermission() expression inside these Method Security Expressions. For example:
@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean add(PublicPost post) {
...
}

How do we enable hasPermission() and where does it get its permissions?
To use hasPermission() expressions, you have to explicitly configure a PermissionEvaluator

Source: Spring Security 3 Reference 15.3.2 Built-In Expressions

The PermissionEvaluator can be a custom implementation or a default Spring Security implementation. For this tutorial, we will do a custom implementation.

Why custom implementation?

Because we want to stay away from the complexity of setting up an ACL database and to make this tutorial simple to learn. Also, the strength of the PermissionEvaluator interface and Expression-Based Access Control is it allows us to construct our own implementation. And since it's our own implementation, we know how it works.

The Custom Map

We'll declare a simple Map that contains a list of ROLES and OBJECTS. Whenever a user tries to access a protected object, it will be checked against this Map. For each ROLE, we assigned what objects it can owned and what permissions it can have.

We have three domain objects that correspond to an actual object in the application.
org.krams.tutorial.domain.AdminPost
org.krams.tutorial.domain.PersonalPost
org.krams.tutorial.domain.PublicPost
We also have three roles that correspond to the roles declared in the authentication-manager
ROLE_ADMIN
ROLE_USER
ROLE_VISITOR
Here's the configuration. Notice we created a new XML configuration to isolate ACL-related configurations.

acl-context.xml
<?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:p="http://www.springframework.org/schema/p" 
 xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/util 
   http://www.springframework.org/schema/util/spring-util-3.0.xsd">
    
    <!-- Declare a simple map containing all our roles --> 
    <util:map id="permissionsMap">
     <entry key="ROLE_ADMIN" value-ref="admin"/>
     <entry key="ROLE_USER" value-ref="user"/>
     <entry key="ROLE_VISITOR" value-ref="visitor"/>
 </util:map>
 
 <!-- Declare permissions for Admin
  Contains a map of objects and their associated allowed actions -->
 <bean id="admin" class="org.krams.tutorial.security.Permission" >
  <property name="objects">
   <map>
    <entry key="org.krams.tutorial.domain.AdminPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
    <entry key="org.krams.tutorial.domain.PersonalPost">
     <list>
      <value>READ</value>
     </list>
    </entry>
    <entry key="org.krams.tutorial.domain.PublicPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
   </map>
  </property>
 </bean>
 
 <!-- Declare permissions for User 
  Contains a map of objects and their associated allowed actions -->
 <bean id="user" class="org.krams.tutorial.security.Permission" > 
  <property name="objects">
   <map>
    <entry key="org.krams.tutorial.domain.PersonalPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
    <entry key="org.krams.tutorial.domain.PublicPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
   </map>
  </property> 
 </bean>
 
 <!-- Declare permissions for Visitor 
  Contains a map of objects and their associated allowed actions -->
 <bean id="visitor" class="org.krams.tutorial.security.Permission" > 
  <property name="objects">
   <map>
    <entry key="org.krams.tutorial.domain.PublicPost">
     <list>
      <value>READ</value>
     </list>
    </entry>
   </map>
  </property> 
 </bean>
</beans>
This configuration matches the requirements we laid in the tables earlier. Here are the tables again:

An admin has READ and WRITE access to everything, but only READ access to the Personal Posts. See the table below:

Admin
Post TypeViewAddEditDelete
Adminxxxx
Personalx
Publicxxxx

A regular user has READ and WRITE access to Personal Posts and Public Posts but only READ access to Admin Posts. See the table below:

User
Post TypeViewAddEditDelete
Admin



Personalxxxx
Publicxxxx

A visitor can only read Admin and Public Posts but no access of whatsoever in the Personal Posts section. See the table below:

Visitor
Post TypeViewAddEditDelete
Admin



Personal
Publicx



The Custom Permission

To store our objects and allowed actions, we used a custom org.krams.tutorial.security.Permission class.

Permission.java
package org.krams.tutorial.security;

import java.util.List;
import java.util.Map;

/**
 * Contains a map of objects and their associated allowed actions
 */
public class Permission {
 
 /**
  *  A Map containing a list of objects and their corresponding actions
  *  <p>
  *  String: key name of the object
  *  List<String>: a list of permissions
  */
 private Map<String, List<String>> objects;
 
 public Map<String, List<String>> getObjects() {
  return objects;
 }
 public void setObjects(Map<String, List<String>> objects) {
  this.objects = objects;
 }

}
This is a simple class containing a Map and a List.

The Custom PermissionEvaluator

Next, we implement the PermissionEvaluator interface by creating a custom class:

CustomPermissionEvaluator.java
package org.krams.tutorial.security;

import java.io.Serializable;
import java.util.Collection;
import java.util.Map;

import org.apache.log4j.Logger;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import javax.annotation.Resource;

/**
 * A custom PermissionEvaluator implementation that uses a Map to
 * check whether a domain Object and access level exists for a particular user. 
 * This also uses RoleHiearchy to retrieve the highest role possible for the user.
 */
public class CustomPermissionEvaluator implements PermissionEvaluator {
        
 protected static Logger logger = Logger.getLogger("security");

 @Resource(name="permissionsMap")
 private Map permissionsMap;
 
 @Resource(name="roleHierarchy")
 private RoleHierarchy roleHierarchy;
 
 /**
  * Evaluates whether the user has permission by delegating to 
  * hasPermission(String role, Object permission, Object domain)
  */
 public boolean hasPermission(Authentication authentication,
   Object targetDomainObject, Object permission) {
  logger.debug("Evaluating expression using hasPermission signature #1");
  
  logger.debug("Retrieving user's highest role");
  String role = getRole(authentication);

  logger.debug("****************");
  logger.debug("role: " + role);
  logger.debug("targetDomainObject: " + targetDomainObject);
  logger.debug("permission: " + permission);
  logger.debug("****************");
 
  // Check the type of object
  logger.debug("User is trying to access the object: " + targetDomainObject);

  logger.debug("Check if user has permission");
  // Delegate to another hasPermission signature
  return hasPermission(role, permission, targetDomainObject);
 }

 /**
  * Another hasPermission signature. We will not implement this.
  */
 public boolean hasPermission(Authentication authentication,
   Serializable targetId, String targetType, Object permission) {
  logger.debug("Evaluating expression using hasPermission signature #2");

  return false;
 }

 /**
  * Retrieves the user's highest role
  */
 private String getRole(Authentication authentication) {
  String highestRole = null;
  
  try {
   Collection auths = roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
   for (GrantedAuthority auth: auths) {
    highestRole = auth.getAuthority();
    break;
   }
   logger.debug("Highest role hiearchy: " + roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()));
   
  } catch (Exception e) {
   logger.debug("No authorities assigned");
  }
  
  return highestRole;
 }
 
 /**
  * Evaluates whether the user has permission
  */
 private Boolean hasPermission(String role, Object permission, Object domain) {
  logger.debug("Check if role exists: " + role);
  if ( permissionsMap.containsKey(role) ) {
   logger.debug("Role exists: " + role);
   
   // Retrieve userPermission object
   Permission userPermission = (Permission) permissionsMap.get(role);
   
   // Check if domain exists in Map
   logger.debug("Check if domain exists: " + domain.getClass().getName());
   if ( userPermission.getObjects().containsKey(domain.getClass().getName())){
    logger.debug("Domain exists: " + domain.getClass().getName());

    // Loop the internal list and see if the class' full name matches
    logger.debug("Check if permission exists: " + permission);
    for (String action: userPermission.getObjects().get(domain.getClass().getName()) ) {
     if (action.equals(permission)) {
      logger.debug("Permission exists: " + action);
      logger.debug("Permission Granted!");
      return true;
     }
    }
   }
  }
  
  // By default, do not give permission
  logger.debug("Permission Denied!");
  return false;
 }
}

Configure Spring Security

Let's use our new classes and enable Spring Security at the same time.

Here's the configuration:

spring-security.xml
<?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:security="http://www.springframework.org/schema/security"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/security 
   http://www.springframework.org/schema/security/spring-security-3.0.xsd">

 <!-- To enable Method Security Expressions and custom PermissionEvaluator
  we need to add the following -->
 <security:global-method-security pre-post-annotations="enabled">
  <security:expression-handler ref="expressionHandler" />
 </security:global-method-security>

 <!-- To use hasPermission() expressions, we have to configure a PermissionEvaluator -->
 <!-- See 15.3.2 Built-In Expression 
   @http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html#el-permission-evaluator -->
 <bean id="expressionHandler"
  class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
  <property name="permissionEvaluator" ref="customPermissionEvaluator" />
  <property name = "roleHierarchy" ref="roleHierarchy"/>
 </bean>
 
 <!-- Declare a custom PermissionEvaluator interface -->
 <bean class="org.krams.tutorial.security.CustomPermissionEvaluator" id="customPermissionEvaluator"/>
  
 <!-- This is where we configure Spring-Security  -->
 <security:http auto-config="true" use-expressions="true" access-denied-page="/krams/auth/denied" >
 
  <security:intercept-url pattern="/krams/auth/login" access="permitAll"/>
  
  <security:form-login
    login-page="/krams/auth/login" 
    authentication-failure-url="/krams/auth/login?error=true" 
    default-target-url="/krams/all/view"/>
   
  <security:logout 
    invalidate-session="true" 
    logout-success-url="/krams/auth/login" 
    logout-url="/krams/auth/logout"/>
 
 </security:http>
 
 <!-- Declare an authentication-manager to use a custom userDetailsService -->
 <security:authentication-manager>
         <security:authentication-provider user-service-ref="userDetailsService">
           <security:password-encoder ref="passwordEncoder"/>
         </security:authentication-provider>
 </security:authentication-manager>
 
 <!-- Use a Md5 encoder since the user's passwords are stored as Md5 in the database -->
 <bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>

  <!-- An in-memory list of users. No need to access an external database layer.
      See Spring Security 3.1 Reference 5.2.1 In-Memory Authentication -->
  <!-- john's password: admin
    jane's password: user
    mike's password: visitor  -->
  <security:user-service id="userDetailsService">
     <security:user name="john" password="21232f297a57a5a743894a0e4a801fc3" authorities="ROLE_ADMIN" />
     <security:user name="jane" password="ee11cbb19052e40b07aac0ca060c23ee" authorities="ROLE_USER" />
     <security:user name="mike" password="127870930d65c57ee65fcc47f2170d38" authorities="ROLE_VISITOR" />
   </security:user-service>
 
 <!-- http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.html -->
 <bean id="roleHierarchy"  class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
     <property name="hierarchy">
         <value>
             ROLE_ADMIN > ROLE_USER
             ROLE_USER > ROLE_VISITOR
         </value>
     </property>
 </bean>
 
</beans>

The key configuration here are the following (the rest are standard Spring Security 3 setup):
<!-- To enable Method Security Expressions and custom PermissionEvaluator
  we need to add the following -->
 <security:global-method-security pre-post-annotations="enabled">
  <security:expression-handler ref="expressionHandler" />
 </security:global-method-security>

 <!-- To use hasPermission() expressions, we have to configure a PermissionEvaluator -->
 <!-- See 15.3.2 Built-In Expression 
   @http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html#el-permission-evaluator -->
 <bean id="expressionHandler"
  class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
  <property name="permissionEvaluator" ref="customPermissionEvaluator" />
  <property name = "roleHierarchy" ref="roleHierarchy"/>
 </bean>
 
 <!-- Declare a custom PermissionEvaluator interface -->
 <bean class="org.krams.tutorial.security.CustomPermissionEvaluator" id="customPermissionEvaluator"/>

<bean id="roleHierarchy"  class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<property name="hierarchy">
<value>
ROLE_ADMIN > ROLE_USER
ROLE_USER > ROLE_VISITOR
</value>
</property>
</bean>

Conclusion

We're done with setting-up the Spring Security section. Our next task is to integrate this application with Spring MVC. Visit the following link for the continuation: Spring Security: Simple ACL using Expression-Based Access Control (Part 2)
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security: Simple ACL using Expression-Based Access Control (Part 1) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

8 comments:

  1. Very Good tutorial! Its helped me lot.

    Thanks for sharing post.

    ReplyDelete
  2. Hi, can I have multiple permission providers? One each for each modules? This is because the business logic implemented is different and complex. Is it possible to configure multiple permission providers in security config file?

    Thanks/Joe

    ReplyDelete
  3. The security at home is essential and must put all our security systemsSecurity on our site is very necessary to do safety systems to give us confidence and tranquility.
    voice and data cabling ct

    ReplyDelete
  4. hi, it was a very good tutorial thanks a lot , i'm just wandering can this be deployed with a jsf framework ? if yes how can i proceed to do it ?

    ReplyDelete
  5. how to do automatic creation ,updation and deletion of acl database entries , as domain objects are added ,updated or deleted

    ReplyDelete
  6. I know, from Spring security we can control user access to certain methods.
    But my question is, can we control button level access based on user role through Spring Security.

    Thanks

    ReplyDelete
  7. I have read your blog its very attractive and impressive. I like it your blog.

    Spring online training Spring online training Spring Hibernate online training Spring Hibernate online training Java online training

    spring training in chennai spring hibernate training in chennai

    ReplyDelete