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]):
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 thesecurity-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 thesecurityAuthorizationContext.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 caseROLE_CLERK
includes the rights ofROLE_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 theauthoritiesByUsernameQuery
. -
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.
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 theACL_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.
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.
Comments
Post a Comment