Spring Security ACL

ACL Spring Security tutorial

By Andrei Tudose and Ovidiu Gheorghies
In this article we show how to implement a simple yet meaningful Spring Security based web application using PostgreSQL as a database backend. The learning curve for effective Spring Security usage was pretty steep for us, and we now write the article we wish we would have had in front of our eyes from the very beginning. As die-hard fans of PostgreSQL for its ease of use, scalability and extensibility, we faced a few perks now and then, but having learned how to deal with them, we are now happy to share.

Most web applications use security to separate those who have to right to perform an action from those who don’t. In its simplest form, this may mean that registered users may post messages, unregistered users may only read them, while administrators can exercise their free will into editing, deleting, banning or generally annoying people. For more complex and realistic applications, one cannot simply use the user role to determine which areas within a web application are available to a certain class of users. For example, it is true that both registered users John and Jane may access the “Edit article” area of the application, yet it would be bad karma to allow John to post messages impersonating Jane, or to allow him to edit the messages of other users by entering in the URL bar a guessed or glimpsed message id. When the business logic of the application requires Jane being able to allow Sam, but not John, to edit her articles, things suddenly become a bit complicated for the application developer.
Luckily, Spring Security (formerly Acegi) comes to the rescue. With it, things do not become simpler than they actually are, yet developers are saved from reinventing and reimplementing the wheel, with all the bugs that this endeavor may bring.
Before proceeding, we remind that ACL stands for “access control list” [1], a list of (subject, action) pairs that can be attached to an object to specify that a given subject or user can perform the corresponding action.
To better explain things, we decided to create a BSD-licensed starter application (called SpringStarter) that exemplifies the concepts we explore, so that we have a source for shameless copy-pasting into our own real projects. The SpringStarter application is attached to this article ([5]), so you can download it and make your own experiments. But first, let us present its business logic.
Imagine an Internet banking application, in which we have two main types of users: customers and clerks. A customer has access to his bank accounts and each bank account contains a list of operations (such as deposit and withdrawal). A clerk can fully administer customer bank accounts (create, read, update, delete), yet he is limited to read-only access to bank account operations. To make things more interesting we decided to attach a different “personal information” type to a customer and a clerk.
The following UML diagram should be worth a thousand words ([6],[7]):
SpringStarter business logic: UML diagram
Here, Clerk, Customer, BankAccount and BankAccountOpperation have their respective meaning. Each Clerk and Customer correspond to precisely one User, which has a list of Authority-s. Any Customer has associated to it a number of BankAccount-s, each of which aggregates BankAccountOperation-s. To make the handling of security-related information uniform throughout the application, we derived Clerk, Customer, BankAccount and BankAccountOperation from AbstractSecureObject – an abstraction to which security information can be specified upon.
Let us dwell now on what needs to be done to make this business logic work with Spring Security. Spring Security uses a series of database tables to store security-related information, and we need to create them.
To spare you the gory theory, we take a hands-on approach: a working application comes with this article. In the archive, you will find the SQL code needed to create the security-related tables in the acl.sql file.
For the lazy, impatient, Linux-running programmers (our favorite kind), executing `cat acl.sql | psql -U user_spring springstarter -h 127.0.0.1` would do it, provided the database is properly configured and that the same password as the one in hibernate.properties is used. Then, it suffices to open and run the Idea project included in the archive (don’t forget to access the “Populate database” link in the web application though, so that you see the credentials to log in with).
Nevertheless, for the sake of respectability, in this article we’ll take it slowly, step by step. So, to make sure that we start off with a clean slate, we’ll drop (then create) these four tables:
    DROP TABLE ACL_ENTRY;
    DROP TABLE ACL_OBJECT_IDENTITY;
    DROP TABLE ACL_CLASS;
    DROP TABLE ACL_SID;
The table ACL_SID essentially lists all the users in our systems. In Spring Security, a “security id” (SID) is assigned to each user or role. This SID can be then used in an access control list (ACL) to specify which actions can the user with that SID perform on the desired objects. In fact, the SID may correspond to an user, a device or a system which can perform an action in the application, or it may correspond to a granted authority such as a role. The distinction between these two possibilities is made by the value store in the principal column of the ACL_SID table: true indicates that the sid is a user, false means that the sid is a granted authority. Note that this table is not concerned with user names, passwords, or other user-related information, as it uses merely the id-s of the users.
    CREATE TABLE ACL_SID (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        principal BOOLEAN NOT NULL,
        sid VARCHAR(100) NOT NULL,
        CONSTRAINT UNIQUE_UK_1 UNIQUE(sid,principal)
    );
Having listed the users, we must also list the objects on which these users can operate. For this, we must inform Spring Security of the Java classes (table ACL_CLASS) and their instances (table ACL_OBJECT_IDENTITY) that are being secured. Let’s now define each of these needed tables.
Each class from the system whose objects we wish to secure must be registered in the ACL_CLASS table and uniquely identified by Spring Security by an id. The class field is the fully qualified Java name of the class, such as com.denksoft.springstarter.BankAccount.
    CREATE TABLE ACL_CLASS (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        class VARCHAR NOT NULL,
        CONSTRAINT UNIQUE_UK_2 UNIQUE(class)
    );
Every secured object in the system is uniquely identified and registered in a single row of the table ACL_OBJECT_IDENTITY. Such row specifies for each object its class id (object_id_class), and its id in the table where objects of this type are stored (object_id_identity). Each object must have an owner, and the owner’s SID is stored in the owner_sid column. If the object was inherited, the fields parent_object and entries_inheriting are used to give the due details.
    CREATE TABLE ACL_OBJECT_IDENTITY (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        object_id_class BIGINT NOT NULL,
        object_id_identity BIGINT NOT NULL,
        owner_sid BIGINT,
        parent_object BIGINT,
        entries_inheriting BOOLEAN NOT NULL,
        CONSTRAINT UNIQUE_UK_3 UNIQUE(object_id_class,object_id_identity),
        CONSTRAINT FOREIGN_FK_1 FOREIGN KEY(parent_object) REFERENCES ACL_OBJECT_IDENTITY(id),
        CONSTRAINT FOREIGN_FK_2 FOREIGN KEY(object_id_class) REFERENCES ACL_CLASS(id),
        CONSTRAINT FOREIGN_FK_3 FOREIGN KEY(owner_sid) REFERENCES ACL_SID(id)
    );
Finally, having listed all the users and all the secured objects (along with their corresponding class), we can now specify what actions can be performed on each of these objects by the desired users (table ACL_ENTRY). Obviously, each row in this table has to contain the reference to the object on which the rights apply (field acl_object_identity), and the user or role id which may perform the action (field sid). The permissions to be granted or denied are specified by using the field mask as a bitfield. For example, if mask is set to the 8-bit value 00000101, then actions 0 and 2 are allowed, while action 1 is disallowed. Note that the meaning of these actions (such as read, write, delete) is application-dependent. The granting field, if set to true, indicates that the permissions indicated by mask are granted to the corresponding sid, otherwise they are revoked or blocked. Luckily, Spring Security takes care of setting the ace_order field for us, so we have one less thing to comment upon.
    CREATE TABLE ACL_ENTRY(
        id BIGSERIAL NOT NULL PRIMARY KEY,
        acl_object_identity BIGINT NOT NULL,
        sid BIGINT NOT NULL,
        mask INTEGER NOT NULL,
        ace_order INT NOT NULL,
        granting BOOLEAN NOT NULL,
        audit_success BOOLEAN NOT NULL,
        audit_failure BOOLEAN NOT NULL,
        CONSTRAINT UNIQUE_UK_4 UNIQUE(acl_object_identity,ace_order),
        CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(acl_object_identity) REFERENCES ACL_OBJECT_IDENTITY(id),
        CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(sid) REFERENCES ACL_SID(id)
    );
You can simply copy-paste the SQL commands above into a psql console connected to your desired database, or you can import the acl.sql from the application archive attached to this article into it.
Below we give an example of how to set up the most important fields of these security-related tables, and how they are connected to actual application business logic objects. In this example, customer John Doe can log in the application with the user name John.Doe. What we want to specify in Spring Security is that this customer has the right to read and write to his bank account, considering that bank accounts are always created by clerks. For this, we need to add a row to the ACL_ENTRY table, by referencing the desired secured object (via acl_object_identity field), the user (via the sid field) and giving the mask of the actions which are allowed (in our case 3, or binary 0011, means read and write access, but no delete or admin). The row in ACL_OBJECT_IDENTITY table states that the BankAccount with object_id_identity set to 10 (“ROXX1212“), whose type is com.denksoft.springstarter.BankAccount (field object_id_class), and which is thus stored in the BankAccount table, was created by the user Clark.Kent (according to field owner_sid).

Spring Security configuration

Let us start the security configuration of the web application by writing the security-config.xml file.
    <?xml version="1.0" encoding="UTF-8"?>

    <beans:beans xmlns="http://www.springframework.org/schema/security"
                 xmlns:beans="http://www.springframework.org/schema/beans"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns:p="http://www.springframework.org/schema/p"
                 xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

http://www.springframework.org/schema/security

                 http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">

         <global-method-security secured-annotations="enabled"/>

         <authentication-provider user-service-ref="userDetailsServiceWrapper" />

         <http>
             <intercept-url pattern="/app/clerk/**" access="ROLE_CLERK"/>
             <intercept-url pattern="/app/customer/**" access="ROLE_CUSTOMER"/>
             <intercept-url pattern="/app/public/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />

             <anonymous/>
             <form-login login-processing-url="/login" login-page="/login.jsp" default-target-url="/app/index.task" />
             <logout logout-url="/logout" logout-success-url="/app/index.task" />
         </http>
    </beans:beans>
This definition should be self-explanatory: we specify that actions in the web directory /app/clerk/** can only be accessed by those who have the role ROLE_CLERK, actions in /app/customer/** can only be accessed by those who have the role ROLE_CUSTOMER, while actions in /app/public/** can be accessed by everybody, including non-authenticated users. Here, we also defined the actions that perform the login and logout operations, the login page, and the default targets the browser is sent to after a successful login and logout.
This should provide us with a basic security mechanism, in which a customer is not allowed to access actions which are specific to clerks. However, we must also ensure that no customer can access or modify the data belonging to another customer. For this, we will use security-specific annotations, with which methods of the service classes are adorned. In effect, we will specify who is allowed to run those methods and with what parameters.

Authorization configuration file

In the securityAuthorizationContext.xml file, we define an interceptor called objectManagerSecurity which will be attached to the function calls we want to secure. This is the root point of the security mechanism.

    <bean id="objectManagerSecurity" class="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor" autowire="byType">
        <property name="accessDecisionManager" ref="businessAccessDecisionManager"/>
        <property name="afterInvocationManager" ref="afterInvocationManager"/>
        <property name="objectDefinitionSource" ref="objectDefinitionSource"/>
    </bean>

    <bean id="objectDefinitionSource" class="org.springframework.security.annotation.SecuredMethodDefinitionSource" />
The objectManagerSecurity bean that we use extends org.springframework.security.intercept.AbstractSecurityInterceptor. You can refer to the Spring Security API to learn more about it, but for now it suffices to say that in our application this bean will intercept the call to methods that bear some security-specific annotations. For these annotations to be taken into account, we need to add this line to the security-config.xml file:
    <global-method-security secured-annotations="enabled"/>
The access decision manager bean used by the objectManagerSecurity, namely businessAccessDecisionManager, decides whether a given user has the right to perform a certain operation on a secured object. It does this by consulting a list of registered voters which cast their opinion on whether a particular access should be allowed or not.

    <bean id="businessAccessDecisionManager" class="org.springframework.security.vote.UnanimousBased">
        <property name="allowIfAllAbstainDecisions" value="true"/>
        <property name="decisionVoters">
            <list>
                <ref local="roleVoter"/>
                <ref local="aclObjectReadVoter"/>
                <ref local="aclObjectWriteVoter"/>
                <ref local="aclObjectDeleteVoter"/>
                <ref local="aclObjectAdminVoter"/>
            </list>
        </property>
    </bean>
A voter implementation returns an integer, whose possible values are given by the static fields ACCESS_ABSTAIN, ACCESS_DENIED and ACCESS_GRANTED. Each voter has a list of permissions with which it can operate, and a reference to the aclService bean, which is used to retrieve the ACL-s. Based on these permission definitions (shown below), and the data returned by the aclService, the voter expresses its opinion on whether access should be granted or not, by returning one of these values. However, the final decision about granting or denying access is taken by the decision manager, typically by considering the values the registered voters return. We use here the UnanimousBased bean, which is a simple concrete implementation of org.springframework.security.AccessDecisionManager that requires all voters to either abstain or grant access if the access is to be granted. Having set the allowIfAllAbstainDecisions property to true, access is granted even if all voters abstain (and obviously none denies the access).
An individual AclEntryVoter has three mandatory constructor arguments. The first one is a reference to the aclService, the second one is the action that is to be secured, and the third one consists in a list of permissions based on which the action can be carried out. We also need to set the property processDomainObjectClass with the class name of the object instances we want to secure. Since in our application we inherit all the objects we wish to secure from AbstractSecureObject, we need one single such XML entry in the securityAuthorizationContext.xml configuration file.
   <bean id="aclObjectReadVoter" class="org.springframework.security.vote.AclEntryVoter">
        <constructor-arg ref="aclService"/>
        <constructor-arg value="ACL_OBJECT_READ"/>
        <constructor-arg>
            <list>
                <ref local="administrationPermission"/>
                <ref local="readPermission"/>
            </list>
        </constructor-arg>
        <property name="processDomainObjectClass" value="com.denksoft.springstarter.entity.AbstractSecureObject"/>
    </bean>
In our application there are other three decision voters that are constructed in the same way. For example, if we want another voter to decide if it grants permission to write to object, simply replace ACL_OBJECT_READ with ACL_OBJECT_WRITE and set the desired permissions in the constructor (writePermission, administrationPermission).
The full voter configuration in the SpringStarter application is defined in the file securityAuthorizationContext.xml. Since all the classes whose objects we wish to secure (Customer, Clerk, BankAccount and BankAccountOperation) are derived from AbstractSecureObject, these four voters are the only ones we need to define for object-related security.
The generic role-based voter, roleVoter, is declared separetely. It reads the ROLE_* configuration settings from the current authenticated principal’s granted authorities (see security-config.xml).
    <bean id="roleVoter" class="org.springframework.security.vote.RoleVoter"/>
We define now the ACL permissions that we will use throughout the application: administration, read, write, and delete. For this, we use the helper class FieldRetrievingFactoryBean which retrieves these values from Spring Security’s default implementation of org.springframework.security.acls.Permission interface.
     <bean id="administrationPermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
        <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
    </bean>

    <bean id="readPermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
         <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.READ"/>
    </bean>

    <bean id="writePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
        <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.WRITE"/>
    </bean>

       <bean id="deletePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
        <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.DELETE"/>
    </bean>
An AclService provides support for working with ACL-secured instances. The MutableAclService is used for creating and storing such information in the database. The Spring Security default implementation JdbcMutableAclService contains a series of queries for this, but since these queries are not compatible with PostgreSQL and there are no setter methods for us to change them, we need to implement our own version of MutableAclService, which we called PostgresqlJdbcMutableAclService. Its full implementation is available in the SpringStarter application.

    <bean id="aclService" class="com.denksoft.springstarter.util.security.PostgresqlJdbcMutableAclService">
        <constructor-arg ref="dataSource"/>
        <constructor-arg ref="lookupStrategy"/>
        <constructor-arg ref="aclCache"/>
    </bean>
To configure the data source properties, we reuse the contents of the hibernate.properties file. In the XML snippet below, the ${...} placeholders are set automagically to their corresponding values from the hibernate.properties file.
    <context:property-placeholder location="WEB-INF/classes/hibernate.properties"/>

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          p:driverClassName="${hibernate.connection.driver_class}"
          p:url="${hibernate.connection.url}"
          p:username="${hibernate.connection.username}"
          p:password="${hibernate.connection.password}"/>
A lookup strategy bean retrieves the ACLs from the database. We use the default Spring Security implementation called BasicLookupStrategy and we configure it to use the data source we defined earlier. In addition, we set a caching layer(aclCache) and a logger (ConsoleAuditLogger).
The aclAuthorizationStrategy is used to determine whether a principal is permitted to call administrative methods on the ACLs. In our case, the principal needs ROLE_ADMIN to call methods that modify permissions.
All these settings come together in the configuration file securityAuthorizationContext.xml. Here, aclAuthorizationStrategy has three required parameters: the first one is the authority needed to change ownership, the second one is the authority needed to modify auditing details, and the third one is the authority needed to change other ACL and ACE details.
    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource"/>
        <constructor-arg ref="aclCache"/>
        <constructor-arg ref="aclAuthorizationStrategy"/>
        <constructor-arg>
            <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger"/>
        </constructor-arg>
    </bean>

    <bean id="aclCache" class="org.springframework.security.acls.jdbc.EhCacheBasedAclCache">
        <constructor-arg>
            <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
                <property name="cacheManager">
                    <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
                </property>
                <property name="cacheName" value="aclCache"/>
            </bean>
        </constructor-arg>
    </bean>

    <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
                <bean class="org.springframework.security.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
                <bean class="org.springframework.security.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>
Let us now see how we can put these definitions to good use. There are two main cases to consider: when the service method returns a single object, and when the service method returns a collection of objects. Let us start with a simple common service layer method that returns a single object:
    Customer getCustomer(long id);
In our application, only principals with permissions to read the given customer should be allowed to obtain it. To make this check, the Customer instance is retrieved and passed to the AclEntryAfterInvocationProvider. If the authenticated object does not have the permission to read it, then the provider will throw AccessDeniesException.

The bean below processes AFTER_ACL_READ configuration settings:
    <bean id="afterAclRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationProvider">
        <constructor-arg ref="aclService"/>
        <constructor-arg>
            <list>
                <ref local="administrationPermission"/>
                <ref local="readPermission"/>

            </list>
        </constructor-arg>
    </bean>
Let us look at what needs to be done to secure a service layer method that returns a collection of objects:
    public List getCustomers();
We use here the afterAclCollectionRead bean, which handles the AFTER_ACL_COLLECTION_READ action:
    <bean id="afterAclCollectionRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider">
        <constructor-arg ref="aclService"/>
        <constructor-arg>
            <list>
                <ref local="administrationPermission"/>
                <ref local="readPermission"/>
            </list>
        </constructor-arg>
    </bean>
The following bean is an implementation of afterInvocationManager, which handles the list of AfterInvocationProvider-s.
    <bean id="afterInvocationManager" class="org.springframework.security.afterinvocation.AfterInvocationProviderManager">
        <property name="providers">
            <list>
                <ref local="afterAclRead"/>
                <ref local="afterAclCollectionRead"/>
            </list>
        </property>
    </bean>

Hierarchical roles and custom user details

In the SpringStarter application we use hierarchical roles (see [4]). In a role hierarchy, a user with a particular role has authorization to do everything that a role lower in the hierarchy is authorized to do. However, for this to happen, the higher role must be allowed to access the lower role.
In addition to using hierarchical roles, we will attach some application-specific data to authenticated users using Hibernate, so that each each role has its specific data type attached to it.
In older versions of Acegi Security, attaching application-specific user details to users was a challange (see [2]), but things have improved in Spring Security. The first step is to define our own custom user details wrapper (CustomUserDetailsWrapper), which contains the additional information on the user, by extending the UserDetailsWrapper from Spring Security.
    public class CustomUserDetailsWrapper extends UserDetailsWrapper {

        private Object userInfo;

        public CustomUserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy) {
            super(userDetails, roleHierarchy);
        }

        public CustomUserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy, Object userInfo) {
            super(userDetails, roleHierarchy);
            this.userInfo = userInfo;
        }

        public Object getUserInfo() {
            return userInfo;
        }
    }
We need to create a user details service wrapper that instantiates the newly defined CustomUserDetailsWrapper. For this, we wrap the default user details service from Spring Security so that authentication and authorization details are automatically retrieved, and use Hibernate to retrieve our custom data.
    public class CustomUserDetailsServiceWrapper extends HibernateDaoSupport implements UserDetailsService {

        private UserDetailsService userDetailsService = null;
        private RoleHierarchy roleHierarchy = null;
        private Class[] userInfoObjectTypes;

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

        public void setUserDetailsService(UserDetailsService userDetailsService) {
           this.userDetailsService = userDetailsService;
        }

        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            for(Class clazz: userInfoObjectTypes) {
                DetachedCriteria query = DetachedCriteria.forClass(clazz).add(Restrictions.eq("user.username", username));
                try {
                    Object info = getHibernateTemplate().findByCriteria(query).get(0);
                    return new CustomUserDetailsWrapper(userDetails, roleHierarchy, info);
                } catch (IndexOutOfBoundsException ex) {
                    /do nothing

                }
            }
            return new CustomUserDetailsWrapper(userDetails, roleHierarchy);
        }

        public UserDetailsService getWrappedUserDetailsService() {
            return userDetailsService;
        }

        public void setUserInfoObjectTypes(Class[] userInfoObjectTypes) {
            this.userInfoObjectTypes = userInfoObjectTypes;
        }
    }
The user detail service is wired into the authentication provider in the security-config.xml file.
    <bean id="userDetailsServiceWrapper"
          class="com.denksoft.springstarter.util.security.CustomUserDetailsServiceWrapper"
          p:roleHierarchy-ref="roleHierarchy"
          p:userDetailsService-ref="usersDetailServiceJdbc"
          p:sessionFactory-ref="sessionFactory">
        <property name="userInfoObjectTypes" >
            <list>
                <value>com.denksoft.springstarter.entity.Clerk
                <value>com.denksoft.springstarter.entity.Customer
            </list>
        </property>
     </bean>

     <bean id="roleHierarchy"
          class="org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl">
        <property name="hierarchy">
            <value>
                ROLE_CLERK > ROLE_CUSTOMER
            </value>
        </property>
    </bean>

    <bean id="usersDetailServiceJdbc"
          class="org.springframework.security.userdetails.jdbc.JdbcDaoImpl"
          p:dataSource-ref="dataSource"
          p:authoritiesByUsernameQuery="select users_username as username, authority from users_authorities inner join authorities on authorities_id=id where users_username = ?"/>

    <bean id="hibernateProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"
          p:location="WEB-INF/classes/hibernate.properties"/>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
          p:hibernateProperties-ref="hibernateProperties"
          p:configLocation="classpath:hibernate.cfg.xml"/>
We give a few additional details on the properties used for the definition of the user details service userDetailsServiceWrapper:
  • roleHierarchy – Spring Security’s implementation of roles hierarchy, in our case ROLE_CLERK includes the rights of ROLE_CUSTOMER, but not viceversa.
  • userDetailsService – The default Jdbc implementation of the user details. Because our table structure is slightly different from the default one, we overwrite the authoritiesByUsernameQuery.
  • userInfoObjectTypes – A list of entities containing application specific information that we want to attach to authenticated users.
  • sessionFactory – Hibernate session factory using Java 5 annotations. The session properties are loaded from a properties file by PropertiesFactoryBean.
The hibernate.cfg.xml file contains only the entity classes used by the session factory.
    <?xml version='1.0' encoding='UTF-8'?>
    <!DOCTYPE hibernate-configuration PUBLIC "-/Hibernate/Hibernate Configuration DTD 3.0/EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

    <hibernate-configuration>

        <session-factory>
            <mapping class="com.denksoft.springstarter.entity.Clerk" />
            <mapping class="com.denksoft.springstarter.entity.security.User" />
            <mapping class="com.denksoft.springstarter.entity.security.Authority" />
            <mapping class="com.denksoft.springstarter.entity.BankAccount" />
            <mapping class="com.denksoft.springstarter.entity.AbstractSecureObject" />
            <mapping class="com.denksoft.springstarter.entity.BankAccountOperation" />
            <mapping class="com.denksoft.springstarter.entity.Customer" />
        </session-factory>
    </hibernate-configuration>

ACL management

Suppose that we wish to grant a certain permission to a given user for a certain object. How do we do it? Obviously, we need to add a row in the ACL_ENTRY table, detailing this permission. To avoid having to do this by hand, we created a proxy interface to the aclSecurityUtil bean, that automates this process and offers an easy-to-use API.
Let us list the XML definitions first, explanations follow.
    <bean id="aclSecurityUtil" class="org.springframework.aop.framework.ProxyFactoryBean">
        <qualifier value="aclSecurity"/>
        <property name="proxyInterfaces" value="com.denksoft.springstarter.util.security.AclSecurityUtil"/>
        <property name="interceptorNames">
            <list>
                <idref local="transactionInterceptor"/>
                <idref local="aclSecurityUtilTarget"/>
            </list>
        </property>
    </bean>

    <bean id="aclSecurityUtilTarget" class="com.denksoft.springstarter.util.security.AclSecurityUtilImpl" p:mutableAclService-ref="aclService"/>

    <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"
          p:transactionManager-ref="jdbcTransactionManager">
        <property name="transactionAttributeSource">
            <value>
                com.denksoft.springstarter.util.security.AclSecurityUtil.deletePermission=PROPAGATION_REQUIRED
                com.denksoft.springstarter.util.security.AclSecurityUtil.addPermission=PROPAGATION_REQUIRED
            </value>
        </property>
    </bean>

    <bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/>
The aclSecurityUtil bean is an AOP proxy to com.denksoft.springstarter.util.security.AclSecurityUtil interface implementation, and it has with two interceptors:
  • the transactionInterceptor, since Spring Security uses JDBC to retrieve ACL-s;
  • the aclSecurityUtilTarget, which is the bean that will be proxied.
The AclSecurityUtil service interface defines the following reasonable-looking methods:
    public interface AclSecurityUtil {
        public void addPermission(AbstractSecureObject securedObject, Permission permission, Class clazz);
        public void addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz);
        public void deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission);
    }
Its corresponding implementation is listed below:
    public class AclSecurityUtilImpl implements AclSecurityUtil {

        private static Log logger = LogFactory.getLog(AclSecurityUtil.class);

        private MutableAclService mutableAclService;

        public void setMutableAclService(MutableAclService mutableAclService) {
            this.mutableAclService = mutableAclService;
        }

        public void addPermission(AbstractSecureObject secureObject, Permission permission, Class clazz) {

            addPermission(secureObject, new PrincipalSid(getUsername()), permission, clazz);
        }

        public void addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) {
            MutableAcl acl;
            ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), securedObject.getId());

            try {
                acl = (MutableAcl) mutableAclService.readAclById(oid);
            } catch (NotFoundException nfe) {
                acl = mutableAclService.createAcl(oid);
            }

            acl.insertAce(acl.getEntries().length, permission, recipient, true);
            mutableAclService.updateAcl(acl);

            if (logger.isDebugEnabled()) {
                logger.debug("Added permission " + permission + " for Sid " + recipient + " securedObject " + securedObject);
            }
        }

        public void deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) {
            ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), securedObject.getId());
            MutableAcl acl = (MutableAcl) mutableAclService.readAclById(oid);

            / Remove all permissions associated with this particular recipient (string equality used to keep things simple)
            AccessControlEntry[] entries = acl.getEntries();

            for (int i = 0; i < entries.length; i++) {
                if (entries[i].getSid().equals(recipient) && entries[i].getPermission().equals(permission)) {
                    acl.deleteAce(i);
                }
            }

            mutableAclService.updateAcl(acl);

            if (logger.isDebugEnabled()) {
                logger.debug("Deleted securedObject " + securedObject + " ACL permissions for recipient " + recipient);
            }
        }

        protected String getUsername() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();

            if (auth.getPrincipal() instanceof UserDetails) {
                return ((UserDetails) auth.getPrincipal()).getUsername();
            } else {
                return auth.getPrincipal().toString();
            }
        }
    }
The addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) function creates or retrieves the ACL of the securedObject and adds a new permission to the desired recipient, while a call to addPermission(AbstractSecureObject securedObject, Permission permission, Class clazz) adds permissions to the current authenticated user. The deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) method removes permissions for the desired recipient. The sid can be a Principal, Sid, or GrantedAuthoritySid.
We show now how this API can be used in the application logic. Since we wish to know exactly the point where ACL operations are performed (as opposed to having these operations scattered and copy-pasted throughout the code), we define a SecurityService interface which handles permission management. In its implementation, we have autowired the aclSecurityUtil bean discussed above, using the qualifier name property from its bean declaration ().
    public class SecurityServiceImpl implements SecurityService {

        @Autowired
        @Qualifier("aclSecurity")
        private AclSecurityUtil aclSecurityUtil;

        public void setCustomerPermissions(Customer customer) {
            Sid sid = new PrincipalSid(customer.getUser().getUsername());
            Sid sidAdmin = new GrantedAuthoritySid("ROLE_CLERK");

            aclSecurityUtil.addPermission(customer, sid, BasePermission.ADMINISTRATION, Customer.class);
            aclSecurityUtil.addPermission(customer, sidAdmin, BasePermission.ADMINISTRATION, Customer.class);
        }
    }
This piece of code code adds administration permissions to the registered customer and to any principal with the role ROLE_CLERK.
Let us now consider an application-level service interface, CustomerService. To secure it, we must attach to it the objectManagerSecurity interceptor in the applicationContext.xml file. Again, we will use the ProxyFactoryBean class to build an AOP proxy around our service implementation.
    <bean id="customerServiceTarget" class="com.denksoft.springstarter.service.impl.CustomerServiceImpl"/>

    <bean id="customerService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <qualifier value="customerService"/>
        <property name="proxyInterfaces" value="com.denksoft.springstarter.service.CustomerService"/>
        <property name="interceptorNames">
            <list>
                <idref bean="objectManagerSecurity"/>
                <idref local="customerServiceTarget"/>
            </list>
        </property>
    </bean>
The interface is adorned with Java 5 annotations:
    public interface CustomerService {
        @Secured({"ROLE_CUSTOMER","AFTER_ACL_READ"})
        public Customer getCustomer(long id);

        @Secured({"ROLE_CUSTOMER","ACL_OBJECT_ADMIN"})
        public void modifyBankAccount(BankAccount bankAccount);

        @Secured({"ROLE_CUSTOMER","AFTER_ACL_COLLECTION_READ"})
        public Collection getCustomerBankAccounts();
    }
After this definition is in place, in order to execute a call to method getCustomer, the calling principal must first have the role ROLE_CUSTOMER. Once the object is retrieved, the objectManagerSecurity kicks in, and its afterInvocationManager method checks if the authenticated user has the rights to read this object before returning it. If the user has the required rights then the object is returned to the user, otherwise, the method throws an exception.
If the user wishes to modify a bank account, he must have the role ROLE_CUSTOMER and also the administration role for the BankAccount instance involved, which is given as function parameter to modifyBankAccount. Note the difference between the pairs AFTER_ACL_READ, AFTER_ACL_COLLECTION_READ and ACL_OBJECT_READ, ACL_OBJECT_ADMIN: the rights in the first pair are used for function calls returning secured objects, while the ones in the second pair are used for function calls that have parameters of secured object type.

Conclusions

Setting up a Spring Security application for effective use is no walk in the park. However, when considering doing these things by hand, with all the bugs and productivity loss that this approach may bring, Spring Security certainly appears as an attractive option, even for relatively simple applications. Once in place, a system like the one we configured gives confidence to the development team that a carefully developed and tested security backbone is in place, allowing for the use of more complex security scenarios for the future, should such needs arise.
We look forward to receiving your feedback on improving the SpringStarter application and on making the explanations here easier to understand.

References


[1] http://en.wikipedia.org/wiki/Access_Control_List
[2] http://www.javalobby.org/java/forums/t91426.html
[3] http://springframework.org/spring-security/site/reference/html/springsecurity.html
[4] http://jira.springframework.org/secure/attachment/12872/HierarchicalRoles.pdf
[5] SpringStarter 0.0.1 application
[6] http://metauml.sourceforge.net/
[7] http://en.wikipedia.org/wiki/Class_diagram

Comments

Popular posts from this blog