Friday, October 10, 2008

Spring Security ACL - very basic tutorial

Introduction


I spent last few days digging into Spring Security User Guide documentation. I had not much experience with this product before, so I thought it is the right time to learn it. I was motivated by the analysis I was doing recently for some project: one of the requirements was building the flexible access control module, allowing for granting fine-grained permissions on per-user, per-object and per-operation basis. This kind of authorization is not possible with standard role-base Java EE approach. I wanted to know how it is solved in real-world systems. As a Spring-addict, my first choice to look at was Spring Security. Spring Security solves the problem with its ACL package.


I read the user guide, and found it not clear enough. It gives some background, but many things are not explained good enough. I started analysing the sample application bundled with Spring Security realease, and JavaDocs, but the complexity of Spring Security was overwhelming. I wanted to implement some simple proof-of-concept application using Spring Source ACL. I believe Spring Security is powerful solution, but amount of interfaces, classes, dependencies, configuration required, makes it hard to learn. Just look at the sample contacs application: the authorization configuration (applicationContext-common-authorization.xml) requires almost 20 spring beans in context, many of them are just infrastructure classes (and it doesn't contain the definitions of actual ACLs). Why there are no simply defaults for most settings? (Take for example the aclService bean: why do I have to explicitely define the cache stategy and lookup strategy? Why there is no constructor assuming default values? as a result, I blindly copy the configuration from sample.)


I was still lost, so I looked around for some tutorial, and found A Spring Security ACL tutorial for PostgreSQL. It looked promising, but it turned out to be not less complicated than contacts sample. After long fight, finally I managed to build my simple application, using Spring Security ACL concepts, but with simplified implementations. It took me long time, and I wasn't able to find any such example on the net - so I decided to publish my results. I hope you will find it helpful in learning Spring Security ACL.


Requirements


Imagine the following scenario. We have a company. Each employee in this company is obliged to provide some work reports to his manager. Manager is allowed to accept the report (or not - but this is not relevant here). Only employee can create report. Only manager can accept it. So far it's easy and can be solved with roles-based solution. The key point, however, is that manager can't accept all reports - only reports submitted by his subordinate employee. Let's say we have four employees and 2 managers. Employees empl1 and empl2 are assigned to manager1, and empl3 and empl4 are assigned to manager2. So manager1 can only accept reports of empl1 and empl2, but not those of empl3 and empl4. This cannot be easily solved with roles. Instead, we will use Spring Security ACLs.


Implementation


Domain model


We have very simple object model here: the Report class with id, description, acceptation flag, and reference to the owner User; and the User class itself, containing only login name field (obviously, this is extremely simplified model when compared with any real applicaton - but this will help us focus on the main subject). Below is the code of our model classes:



package springacltutorial.model;

public class User {

public User(String login) {
this.login = login;
}

private String login;

public String getLogin() {
return login;
}
}


package springacltutorial.model;

public class Report {

private long id;
private String description;
private boolean accepted;
private User user;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public boolean isAccepted() {
return accepted;
}

public void setAccepted(boolean accepted) {
this.accepted = accepted;
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}
}

Service layer and data access


Let's implement our service class, named ReportsServices. It has only two methods: one for creating new reports (by employees) and second one for accepting reports (by managers). We also use simple map-based DAO for accessing the reports. For simplification, we will not use separate interface and class implementation, but only concrete classes. Spring is able to handle it with help of CGLIB. Here is the code of the classes:



package springacltutorial.services;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.Authentication;
import org.springframework.security.annotation.Secured;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import springacltutorial.dao.ReportsDao;
import springacltutorial.model.Report;
import springacltutorial.model.User;

@Service
public class ReportServices {

@Autowired
ReportsDao dao;

// this method should be accessible only to users with role EMPLOYEE
public long addReport(String description) {
Report report = new Report();
report.setDescription(description);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
if (auth.getPrincipal() instanceof UserDetails) {
report.setUser(new User(((UserDetails) auth.getPrincipal()).getUsername()));
} else {
report.setUser(new User(auth.getPrincipal().toString()));
}
}
dao.saveReport(report);
return report.getId();
}

// this method may be called only by user with role MANAGER, being manager
// of user linked to the report
public void acceptReport(Report report) {

report.setAccepted(true);
}

}


package springacltutorial.dao;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Repository;

import springacltutorial.model.Report;

@Repository
public class ReportsDao {

private long sequence;
private Map<Long, Report> reports = new HashMap<Long, Report>();

public void saveReport(Report report) {
if (report.getId() == 0) {
report.setId(++sequence);
}
reports.put(report.getId(), report);
}

public Report getReportById(long reportId) {
return reports.get(reportId);
}

}

The method addReport must make sure that the owner of the new report is the currently logged-in user. Because of this, we had to add some Spring Security specific code to get the name of current user. This seems not the ideal solution, but I cannot find anything better than this. As you also see, I use Spring 2.5 stereotype annotations, so that I don't have to enlist my beans in Spring config file. Here is the Spring config file applicationContext-business.xml:



<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

<context:component-scan base-package="springacltutorial"/>

</beans>

At this moment, we have most of our application ready. We won't focus on client code calling the service in this tutorial, as this doesn't matter much. Instead, let's write some tests. First the test focusing only on functional site of the service class:



package springacltutorial.services;

import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import springacltutorial.dao.ReportsDao;

import static org.junit.Assert.*;

public class ServicesTest {

@Before
public void setup() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext-business.xml");
reportServices = (ReportServices) BeanFactoryUtils.beanOfType(context, ReportServices.class);
dao = (ReportsDao) BeanFactoryUtils.beanOfType(context, ReportsDao.class);
}

ReportServices reportServices;
ReportsDao dao;

@Test
public void testAddReport() {
long id = reportServices.addReport("springacltutorial");
assertEquals("springacltutorial", dao.getReportById(id).getDescription());
}

@Test
public void testAcceptReport() {
long id = reportServices.addReport("springacltutorial");
assertEquals(false, dao.getReportById(id).isAccepted());
reportServices.acceptReport(dao.getReportById(id));
assertEquals(true, dao.getReportById(id).isAccepted());
}

}

For building and running the application, following Maven pom.xml file will be helpful:



<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>springacltutorial</groupId>
<artifactId>springacltutorial</artifactId>
<packaging>jar</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>springacltutorial</name>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core-tiger</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

If you run the test now, it should pass. So - we have the functional application, but without any security imposed on it. Let's add authorization now.


Authorization


Now we come to the main point of this tutorial: authorization. First we add the second spring context file, named applicationContext-security.xml. Initially, it will contain definition of UserService (logins, passwords and roles of users):



<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">

<security:authentication-provider >
<security:password-encoder hash="plaintext"/>
<security:user-service>
<security:user name="empl1" password="pass1" authorities="ROLE_EMPLOYEE" />
<security:user name="empl2" password="pass2" authorities="ROLE_EMPLOYEE" />
<security:user name="empl3" password="pass3" authorities="ROLE_EMPLOYEE" />
<security:user name="empl4" password="pass4" authorities="ROLE_EMPLOYEE" />
<security:user name="manager1" password="pass1" authorities="ROLE_MANAGER" />
<security:user name="manager2" password="pass2" authorities="ROLE_MANAGER" />
<security:user name="testUser" password="" authorities=""/>
</security:user-service>
</security:authentication-provider>

</beans>

Now we can write a test:



package springacltutorial.services;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.AccessDeniedException;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;

import springacltutorial.dao.ReportsDao;
import springacltutorial.model.Report;

public class ServicesAuthorizationTest {

@Before
public void setup() {
SecurityContextHolder.getContext().setAuthentication(null);
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "applicationContext-business.xml", "applicationContext-security.xml" });
reportServices = (ReportServices) BeanFactoryUtils.beanOfType(context, ReportServices.class);
dao = (ReportsDao) BeanFactoryUtils.beanOfType(context, ReportsDao.class);
}

ReportServices reportServices;
ReportsDao dao;

@Test
public void testAddReport() {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("empl1", "pass1"));
reportServices.addReport("springacltutorial");
// now use user without EMPLOYEE role
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("testUser", ""));
try {
reportServices.addReport("springacltutorial");
fail("should throw AccessDeniedException");
} catch (AccessDeniedException e) {
return; // ok
}
}

@Test
public void testAcceptReport() {
// empl1 creates report
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("empl1", "pass1"));
Report reportEmpl1 = dao.getReportById(reportServices.addReport("springacltutorial"));

// empl3 creates report
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("empl3", "pass3"));
Report reportEmpl3 = dao.getReportById(reportServices.addReport("springacltutorial"));

// manager1 accepts report of empl1 - ok
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("manager1", "pass1"));
assertEquals(false, reportEmpl1.isAccepted());
reportServices.acceptReport(reportEmpl1);
assertEquals(true, reportEmpl1.isAccepted());

// manager1 tries to accept report of empl3 - access denied
assertEquals(false, reportEmpl3.isAccepted());
try {
reportServices.acceptReport(reportEmpl3);
fail("manager1 cannot accept reports of empl3");
} catch (AccessDeniedException e) {
// ok
}
assertEquals(false, reportEmpl3.isAccepted());

// manager2 accepts report of empl3 - ok
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("manager2", "pass2"));
reportServices.acceptReport(reportEmpl3);
assertEquals(true, reportEmpl3.isAccepted());
}

}

If you run the test, it will fail, because there is no access control imposed on methods. This is what we have to do now.


Protecting addReport method is simple: each user with granted role EMPLOYEE can call it; user without this role can't. So we simply specify the role based authorization access for this method. We can do it from context configuration file, but it is simpler with annoation:



@Secured("ROLE_EMPLOYEE")
public long addReport(String description) {
...

Note that default Spring Security voter which deals with roles takes into account only those configuration attributes that match the pattern "ROLE_*", this is why we called the role ROLE_EMPLOYEE, not simply EMPLOYEE.


In case of acceptReport method by analogy we can use annoation @Secured("ROLE_MANAGER"). This is ok, but not enough. We have following requirement: the user may call this method if he has manager role, and is the manager of the user who created the report. So we put the second configuration attribute on the method, this time called freely, let it be ACL_REPORT_ACCEPT:



@Secured( { "ROLE_MANAGER", "ACL_REPORT_ACCEPT" })
public void acceptReport(Report report) {
...

That's all we have to do in the ReportServices class. Now we switch to the configuration file applicationContext-security.xml and define the mechanism for processing role-based access (with standard RoleVoter - easy part) and the mechanism for processing ACL-based access (Spring Security ACL - hard part).


ACL mechanism


I assume that you already at least scanned through Spring Security user guide, and you know some basic notions of ACL concept described there. I must say that I found the section in user guide describing ACLs not clear enough. It does not highlight the key interfaces and relationships betweent them, but instead it jumps fast into the JDBC-based implementation, desribing the 4 tables that must be set up for this implementation. This gives the impression that this is the only way of using ACLs, and that the model of those tables actually the model of ACLs. But this is not. The JDBC-based implementation is, as far as I understand, the production-ready implementation, and uses some optimisations, like efficient SQL queries, caches, lazy loading etc. The database model is not in 1:1 relationship to the core ACL model.


I prefer approaching it in a different way: first learn the core model (interfaces). Than look at some extremely simple implementation, without any optimisation, persistence etc (if possible) and check if it works and if the model/API fits my expectations. Only then look at production-ready implementations, with all their complexity.


So let's look at ACL model. The main interface is Acl, representing access control list for given domain object. It contains list of AccessControlEntry objects, and the ObjectIdentity. Interface ObjectIdentity provides indirect represention of domain object. Acl doesn't keep direct reference to domain object for which this ACL was defined - instead it uses ObjectIdentity. ObjectIdentity knows the type of Java class of the domain object, and identifier of the specific instance of this class. How this identifier is defined and retrieved depends on implementation. Default implementation assumes that domain object has the method getId(), returning type compatible with long. AccessControlEntry (ACE) represent individual permission assignment. It has unique id, reference to Acl itself (note: bidirectional relationship between ACL and each ACE), the Permission, and the Sid. Interface Sid (stands for: Secure Identity) is a kind of common abstraction for user names (PrincipalSid implementation) and user roles (GrantedAuthoritySid implementation). So here Sid represents user (or role) to whom the Permission is granted (or revoked, depending on granting flag). So each ACE means: the specific Sid (user or role) is granted or revoked the specific Permission (read, write, delete, accept, ...) on the domain object for which the ACL is defined.


Collection of ACEs and ObjectIdentity are two main components of Acl. Additionally Acls can build inheritance trees, so each Acl can optionally reference its parent (if implementation supports it). Also optionally, Acl can have the owner, represented by Sid object, if implementation supports it. Each ACE must be immutable. ACL can be mutable (for this purpuse there is a specific subinterface: MutableAcl, with methods like insertAce, deleteAce). Acl contains one "real" method: isGranted, containing actual authorization logic.





ACL model interfaces constitute one side of the whole picture. The second side is ACL access API. The central interface here is AclService, providing retrieval of ACLs for given ObjectIdentity, through the set of overloaded readAclById and readAclsById methods. The whole picture is completed by the AclEntryVoter class, which is an ACL-specific implementation of the standard Spring Security AccessDecisionVoter interface. It is responsible for giving "opinions" (votes) about whether the current user should be granted access for protected resource or not (opinion may be of three types: "grant him access", "deny access", or "that's not my business, so I abstain from voting"). The final decision about granting user the access is taken by AccessDecisionManager, which takes into account all the votes of all registered voters (depending on different implementations, the final decision may differ).





How does it all work?


Let's take the method acceptReport from our tutorial application. The method itself is the protected resource, and we want to restrict access to it only to users with MANAGER role, and what's more only actual manager of the user who created the report should be able to accept it. As you remember, we put on the method the restriction annotation with two configuration attributes: @Secured( { "ROLE_MANAGER", "ACL_REPORT_ACCEPT" }). For that to work, we have to define two voters. First is the standard RoleVoter. When AccessDesicionManager asks this voter about its opinion on the first configuration attribute (ROLE_MANAGER), this voter will see that the parameter starts with "ROLE_" prefix, so it will check if the currently operating user has the ROLE_MANAGER role. If it has this role, voter will vote for "grant him access". Otherwise, it will vote for "deny access". Then AccessDesicionManager asks this voter about its opinion on the second configuration attribute (ACL_REPORT_ACCEPT), and the voter will abstain from voting, because it does not match the ROLE_* pattern. This way we have done with the role checking.


Now we have to define the second voter, this time of type AclEntryVoter, with following configuration: reference to AclService; the string with configuration attribute that should trigger the voter to actually vote ("ACL_REPORT_ACCEPT" in our case); the list of required permissions that must be found in ACL for this user to grant him access; and the Java type of the domain object, for which the ACL will be used. During method call, the voter analyses all the method parameters and searches for the one compatible with the specified Java type. Once found, the voter will ask the actual object passed under this parameter for the identificator (how? we said earlier: by deafult, it expects the object to have method getId() returning type compatible with long). Having Java type and id, the voter constructs ObjectIdentification from them and then asks AclService for the Acl linked to this ObjectIdentification. After obtaining Acl, it calls its method isGranted, passing the list of required permissions as configured for this voter, and array of Sids of current user. The outcome of this method (boolean value) decides on the voter outcome (grant or deny). If the configuration attribute passed to this voter during call from AccessDesicionManager is different than the one for which this voter is configured, the voter will abstain from voting. In our example application the AccessDesicionManager will ask the AclEntryVoter first for ROLE_MANAGER, and voter will abstain, and then ask for ACL_REPORT_ACCEPT and this time voter will voter "grant" or "deny", depending on current user, the domain object passed as protected method argument, and the ACL configurations. After calling two voters for two configuration parameters, the AccessDesicionManager will finally decide on granting access or not, based on collected votes.


As you see, we have to configure following beans in Spring context: the AccessDecisionManager implementation fed with two voters, namely RolesVoter and AclEntryVoter; and the AclService implementation to be used by AclEntryVoter. Additionally we can define our custom ACCEPT Permission (there are five built-in permissions: READ, WRITE, CREATE, DELETE, ADMINISTRATION - we may use any of it, but neither fits perfectly our needs, so we can easily define our own).


Spring security provides us with several AccessDecisionManager implementations - we will use UnanimousBased, as we require that both voters vote for "yes" before we let the method call continue. Spring Securiry contains also one production-ready implementation of AclService, namely JdbcAclService and its subclass JdbcMutableAclService. This class stores and retrieves ACLs to and from database. It requires the predefined structure of 4 tables - you may find details in Spring Security documentation. Configuration of this implementation is complex, too complex for simple prototyping solutions in my opinion. Because of this, we're going to implement this service on our own, to use in-memory stored ACLs. We will also write our own implementation of Acl, beacue the built-in AclImpl implements several other interfaces, which makes it unnecessarily complicated.


It's right time to implement it


First we code our ACCEPT permission, extending the built-in BasePermission class:



package springacltutorial.infrastructure;

import org.springframework.security.acls.Permission;
import org.springframework.security.acls.domain.BasePermission;

/**
* Adds ACCEPT permission to standard set of Spring Security permissions.
* Based on BasePermission code.
*/
public class ExtendedPermission extends BasePermission {

private static final long serialVersionUID = 1L;

public static final Permission ACCEPT = new ExtendedPermission(1 << 5, 'a'); // 32

/**
* Registers the public static permissions defined on this class. This is
* mandatory so that the static methods will operate correctly. (copied from
* super class)
*/
static {
registerPermissionsFor(ExtendedPermission.class);
}

private ExtendedPermission(int mask, char code) {
super(mask, code);
}
}

The char code is used only for textual-representation of the permission, and has no influence on any logic. BasePermission uses capital letter 'A' for ADMINISTRATION, so I used 'a' for ACCEPT. As I said, it really doesn't matter. We used sixth bit (1 << 5) in the internal Permission implementation (first five bits are used by built-in types). I will not dig into internals of Permissions concept, as it is well described in Spring security documentation and not very difficult. Now we turn our attation to Acl implementation:



package springacltutorial.infrastructure;

import org.springframework.security.acls.AccessControlEntry;
import org.springframework.security.acls.Acl;
import org.springframework.security.acls.NotFoundException;
import org.springframework.security.acls.Permission;
import org.springframework.security.acls.domain.AuditLogger;
import org.springframework.security.acls.domain.ConsoleAuditLogger;
import org.springframework.security.acls.objectidentity.ObjectIdentity;
import org.springframework.security.acls.sid.Sid;

/**
* Very simple implementation of Acl interface, based on
* org.springframework.security.acls.domain.AclImpl source (mainly the isGranted
* method code). This implementation neither use owner concept nor parent
* concept.
*/
public class SimpleAclImpl implements Acl {

private static final long serialVersionUID = 1L;
private ObjectIdentity oi;
private AccessControlEntry[] aces;
private transient AuditLogger auditLogger = new ConsoleAuditLogger();

public SimpleAclImpl(ObjectIdentity oi, AccessControlEntry[] aces) {
this.oi = oi;
this.aces = aces;
}

public AccessControlEntry[] getEntries() {
return aces;
}

public ObjectIdentity getObjectIdentity() {
return oi;
}

public Sid getOwner() {
return null; // owner concept is optional, we don't use it
}

public Acl getParentAcl() {
return null; // we don't use inheritance
}

public boolean isEntriesInheriting() {
return false; // we don't use inheritance
}

public boolean isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) throws NotFoundException {

AccessControlEntry firstRejection = null;

for (int i = 0; i < permission.length; i++) {
for (int x = 0; x < sids.length; x++) {
// Attempt to find exact match for this permission mask and SID
boolean scanNextSid = true;

for (int j = 0; j < aces.length; j++) {
AccessControlEntry ace = (AccessControlEntry) aces[j];

if ((ace.getPermission().getMask() == permission[i].getMask()) && ace.getSid().equals(sids[x])) {
// Found a matching ACE, so its authorization decision
// will prevail
if (ace.isGranting()) {
// Success
if (!administrativeMode) {
auditLogger.logIfNeeded(true, ace);
}

return true;
} else {
// Failure for this permission, so stop search
// We will see if they have a different permission
// (this permission is 100% rejected for this SID)
if (firstRejection == null) {
// Store first rejection for auditing reasons
firstRejection = ace;
}

scanNextSid = false; // helps break the loop

break; // exit "aces" loop
}
}
}

if (!scanNextSid) {
break; // exit SID for loop (now try next permission)
}
}
}

if (firstRejection != null) {
// We found an ACE to reject the request at this point, as no
// other ACEs were found that granted a different permission
if (!administrativeMode) {
auditLogger.logIfNeeded(false, firstRejection);
}

return false;
}

throw new NotFoundException("Unable to locate a matching ACE for passed permissions and SIDs");
}

public boolean isSidLoaded(Sid[] sids) {
// we use in-memory structure, not external DB, so all entries are
// always loaded
// (if I correctly understand meaning of this method)
return true;
}
}

As you see, it is pretty straightforward. The only logic is contained in isGranted method - which is a slightly simplified copy of the code from the standard Spring Security AclImpl class. The usage of AuditLogger is not necessary, but it was already there, so I decided to use it - it may be helpful to see on the console what's going on inside.


Finally, we need AclService implementation:



package springacltutorial.infrastructure;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.security.acls.AccessControlEntry;
import org.springframework.security.acls.Acl;
import org.springframework.security.acls.AclService;
import org.springframework.security.acls.NotFoundException;
import org.springframework.security.acls.domain.AccessControlEntryImpl;
import org.springframework.security.acls.objectidentity.ObjectIdentity;
import org.springframework.security.acls.objectidentity.ObjectIdentityImpl;
import org.springframework.security.acls.sid.PrincipalSid;
import org.springframework.security.acls.sid.Sid;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import springacltutorial.model.User;

/**
* The simplest possible implementation of AclService interface. Uses in-memory
* collection of ACLs, providing fast and easy access to them.
*
*/
@Service
public class InMemoryAclServiceImpl implements AclService {

Map<ObjectIdentity, Acl> acls = new HashMap<ObjectIdentity, Acl>();

@PostConstruct
public void initializeACLs() {
// create ACLs according to requirements of tutorial application
ObjectIdentity user1 = new ObjectIdentityImpl(User.class, "empl1");
ObjectIdentity user2 = new ObjectIdentityImpl(User.class, "empl2");
ObjectIdentity user3 = new ObjectIdentityImpl(User.class, "empl3");
ObjectIdentity user4 = new ObjectIdentityImpl(User.class, "empl4");

Acl acl1 = new SimpleAclImpl(user1, new AccessControlEntry[1]);
acl1.getEntries()[0] = new AccessControlEntryImpl("ace1", acl1, new PrincipalSid("manager1"), ExtendedPermission.ACCEPT, true, true, true);
acls.put(acl1.getObjectIdentity(), acl1);
Acl acl2 = new SimpleAclImpl(user2, new AccessControlEntry[1]);
acl2.getEntries()[0] = new AccessControlEntryImpl("ace2", acl2, new PrincipalSid("manager1"), ExtendedPermission.ACCEPT, true, true, true);
acls.put(acl2.getObjectIdentity(), acl2);
Acl acl3 = new SimpleAclImpl(user3, new AccessControlEntry[1]);
acl3.getEntries()[0] = new AccessControlEntryImpl("ace3", acl3, new PrincipalSid("manager2"), ExtendedPermission.ACCEPT, true, true, true);
acls.put(acl3.getObjectIdentity(), acl3);
Acl acl4 = new SimpleAclImpl(user4, new AccessControlEntry[1]);
acl4.getEntries()[0] = new AccessControlEntryImpl("ace4", acl4, new PrincipalSid("manager2"), ExtendedPermission.ACCEPT, true, true, true);
acls.put(acl4.getObjectIdentity(), acl4);
}

public ObjectIdentity[] findChildren(ObjectIdentity parentIdentity) {
// I'm not really sure what this method should do...
throw new UnsupportedOperationException("Not implemented");
}

public Acl readAclById(ObjectIdentity object, Sid[] sids) throws NotFoundException {
Map<ObjectIdentity, Acl> map = readAclsById(new ObjectIdentity[] { object }, sids);
Assert.isTrue(map.containsKey(object), "There should have been an Acl entry for ObjectIdentity " + object);

return map.get(object);
}

public Acl readAclById(ObjectIdentity object) throws NotFoundException {
return readAclById(object, null);
}

public Map<ObjectIdentity, Acl> readAclsById(ObjectIdentity[] objects) throws NotFoundException {
return readAclsById(objects, null);
}

public Map<ObjectIdentity, Acl> readAclsById(ObjectIdentity[] objects, Sid[] sids) throws NotFoundException {
Map<ObjectIdentity, Acl> result = new HashMap<ObjectIdentity, Acl>();

for (ObjectIdentity object : objects) {
if (acls.containsKey(object)) {
result.put(object, acls.get(object));
} else {
throw new NotFoundException("Unable to find ACL information for object identity '" + object.toString() + "'");
}
}

return result;
}

}

As you see, I put the initialization method which populates the acls map with data needed for this tutorial (in any real-life application such configuration wouldn't be hardcoded in Java class). We define four ACLs with single ACE each: Two for manager1 (accept reports of empl1 and empl2) and similar two for manager2 (accept reports of empl3 and empl4). Note that the requirement of bidirectional relationship between ACL and each ACE makes the initialization code more verbose (we cannot fully initilize ACL in one line).


We are almost done, but there are two problems left


First problem: we don't want to define the ACL for each report, because reports will be created on the fly by system users, and we don't want to be forced to keep and maintain separate ACL for each report. On the other hand, users are not added and removed from the system too often, so User object is perfect choice as a domain object for which ACLs will be defined - and this is what we did in the initialization method of our service. So now we will have to configure our AclEntryVoter passing it User.class as a configuration parameter. This means, that during secured method call the voter will scan all method parameters searching for User type (I described this procedure earlier). You see the problem? Our method acceptReport does not have User parameter - it takes only Report! Three solutions come to my mind. First: define ACLs for reports, not for users. But as I said earlier, this would be impractical. Second: add second parameter of type User to the method. I don't like it too: I don't like the fact that I have to tweak service interface for security framwork needs, and besides this would be dangerous - what if the caller passes as argument the user not being really owner of the report? The method can obviously do the check that the user passed as argument is equal to the user stored in report as owner - but this is bad too. Third solution: overwrite the way the voter retrieves the domain object. This seems the best one, so we'll use it. Normally, the voter would take the Report object passed as the acceptReport method argument. We don't want this Report object to be passed to AclService: instead we would like to call the getUser() method on this object and pass the User object returned from this method. Fortunately, the AclEntryVoter contains property internalMethod, which does exactly that: it keeps the name of the method that must be called on domain object to retrieve actual object to be passed to the service. So we set this property to the value "getUser".


This solves the first problem. The second problem we have to deal with is following: as you remeber, the default implementation of AclEntryVoter expects that each domain object has a method getId returning type compatible with long, for building the ObjectIdentity and finding ACL bound to it. Our User domain object does not have such method. It's ID is a login of String type (like "empl1"). Having it in mind, I declared the ACLs in the InMemoryAclServiceImpl initilization method with those string-based identifiers. There are two possible solutions for the problem. First: refactor User class and ACLs to use long-based identifiers. This is a good solution, and in production system I would problably go this way. Using artificial identifiers is in most cases the best way to go. Here though we'll use second solution: change the way Spring Security identifies the domain object. In this case it is easy, because AclEntryVoter uses the ObjectIdentityRetrievalStrategy, so we can simply implement this interface and plug it in into our voter using Spring context configuration.



package springacltutorial.infrastructure;

import org.springframework.security.acls.objectidentity.ObjectIdentity;
import org.springframework.security.acls.objectidentity.ObjectIdentityImpl;
import org.springframework.security.acls.objectidentity.ObjectIdentityRetrievalStrategy;
import springacltutorial.model.User;

/** overwrite the strategy: build ObjectIdentity based on user object login property,
* instead of Spring Security default getId() call
*/
public class UserNameRetrievalStrategy implements ObjectIdentityRetrievalStrategy {

public ObjectIdentity getObjectIdentity(Object domainObject) {
User user = (User) domainObject;
return new ObjectIdentityImpl(User.class, user.getLogin());
}

}

The last thing to do is Spring context configuration:



<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">

<security:authentication-provider >
<security:password-encoder hash="plaintext"/>
<security:user-service>
<security:user name="empl1" password="pass1" authorities="ROLE_EMPLOYEE" />
<security:user name="empl2" password="pass2" authorities="ROLE_EMPLOYEE" />
<security:user name="empl3" password="pass3" authorities="ROLE_EMPLOYEE" />
<security:user name="empl4" password="pass4" authorities="ROLE_EMPLOYEE" />
<security:user name="manager1" password="pass1" authorities="ROLE_MANAGER" />
<security:user name="manager2" password="pass2" authorities="ROLE_MANAGER" />
<security:user name="testUser" password="" authorities=""/>
</security:user-service>
</security:authentication-provider>

<security:global-method-security secured-annotations="enabled" access-decision-manager-ref="businessAccessDecisionManager"/>

<!-- Decision manager uses two voters: one is role-based, another is ACL-based -->
<bean id="businessAccessDecisionManager" class="org.springframework.security.vote.UnanimousBased">
<property name="allowIfAllAbstainDecisions" value="true"/>
<property name="decisionVoters">
<list>
<bean id="roleVoter" class="org.springframework.security.vote.RoleVoter"/>
<ref local="aclReportAcceptVoter"/>
</list>
</property>
</bean>


<!-- An access decision voter that reads ACL_REPORT_ACCEPT configuration settings -->
<bean id="aclReportAcceptVoter" class="org.springframework.security.vote.AclEntryVoter">
<constructor-arg ref="aclService"/>
<constructor-arg value="ACL_REPORT_ACCEPT"/>
<constructor-arg>
<list>
<util:constant id="acceptPermission" static-field="springacltutorial.infrastructure.ExtendedPermission.ACCEPT"/>
</list>
</constructor-arg>
<property name="internalMethod" value="getUser"/>
<property name="objectIdentityRetrievalStrategy">
<bean class="springacltutorial.infrastructure.UserNameRetrievalStrategy"/>
</property>
<!-- this is tricky! We have to use Report here, so that voter find it in protected method parameters; "internalMethod" will convert it to User -->
<property name="processDomainObjectClass" value="springacltutorial.model.Report"/>
</bean>

<bean id="aclService" class="springacltutorial.infrastructure.InMemoryAclServiceImpl"/>

</beans>

You can run again the ServicesAuthorizationTest now: it should pass, meaning that Spring Security properly controls the access to methods based on ACL. Well done!