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!

51 comments:

Unknown said...

Excellent work...Thanks buddy

Sujith

Anonymous said...

Good Job!!!
Thanks ....

Wandrey said...

Great work!
Thanks a lot!

Can you try security domain like this?
User has Role
Role has Permission
Permission is a combination of a action and a object.
For example
ROLE_ADMIN, ACTION=APROVE, OBJECT=DOCUMENT
User user1 has role ROLE_ADMIN, role ROLE_ADMIN has permission(APROVE, DOCUMENT) or
permission(APROVE, DOCUMENT(id=1)) or
permission(APROVE, DOCUMENT(number=1,year=2008))

Client code like this:
@Security(role="ROLE_ADMIN",object="DOCUMENT",action="APROVE")
doSomething()
doSomething(Documento document)

Thanks.

Grzegorz Borkowski said...

@wandrey:
I'm not sure what you are asking me for, but I'll try to comment on your approach.
I'm not sure if Spring Security allows you OOTB to bind some permissions with roles (i.e. to say "the ROLE_ADMIN implies permissions X, Y, Z").
Permission is the class from ACL package, so we should probably seek such functionality in this package only. You can use AccessControlEntry (ACE) for this: create ACE linked to some Permission, and to the GrantedAuthoritySid implementation of Sid. If this GrantedAuthority is a role, this will mean that this role has given Permission. But one ACE can be linked to only one Permission, so you must create separate ACE for each permission linked to that role. BUT: the real problem is that each ACE must be linked to exactly one ACL, and each ACL is linked to exactly one ObjectIDentity (i.e. domain object). So you must copy this role-permission association for every object in your system, and would be probably inpractical. This is why I think it is not possible to do OOTB.

Second point: Permission is Spring Security is not, as I understand, a combination of action and object - instead, ACL is. So in case of your example you can create ACL for object=Document(id=1) with ACE granting APPROVE action to the Sid=ROLE_ADMIN (i.e. you must use GrantedAuthoritySid implementation for this ACE - I'm not sure how it is distinguished in database table in default Spring Security JDBC implementation of ACL service).

Then you create acl voter configred with domain object Document, required permission APPROVE, and some arbitraty configuration attribute string. e.g. "ACL_DOCUMENT_APPROVE". With this setup, you simply mark your method this way:
@Secure("ACL_DOCUMENT_APPROVE")
doSomething(Document document).

Wandrey said...

Thanks for your explanation.
I will try your aproach and implement the necessary interfaces implementation.
@Secure("ACL_DOCUMENT_APPROVE"), resolve my problem.
I will try a jdbc decisionManager.

Thanks!

Anonymous said...

Nice job documenting this. There really isnt much out there that dares to go into the ACL side of Spring Security and the docs curiously skip of the whole thing!

Also worth checking out http://server.denksoft.com/wordpress/?page_id=5

Martin Kuhn said...

Hi,
thanks for this good explanation.
Is the source code of your tutorial available for download?

TIA
Martin

Grzegorz Borkowski said...

@Martin
No, but the article should actually contain the whole code, I think.

anorakgirl said...

This is a great tutorial, just what I needed, as I don't want to use the supplied JdbcAclService. I tried to follow the tutorial, but when I add the AclVoter to my accessDecisionManager, I get the error:
java.lang.IllegalArgumentException: AccessDecisionManager does not support secure object class: class org.springframework.security.intercept.web.FilterInvocation

I can't work out what to do about this, any ideas?
Thanks

Grzegorz Borkowski said...

@anorakgirl:
The tutorial itself don't use HTTP-level security, only method-level, in order to focus only on the main topic: ACLs. In other words, it doesn't use <http> element in spring configuration file. The exception you obtained suggests that you added HTTP-level security, and configured it somehow wrong way, I guess.

Grzegorz Borkowski said...

I've just found that in AclEntryVoter there is an "internalMethod" property, which can be used to solve the problem of extracting User from Report. This means that overriding the AclEntryVoter (as I shown earlier) is not needed at all. I've updated the last section of the tutorial to use this simpler solution.

Alvin said...

Very nice documentation..Thank you. Just want to add this dependency if you are using JDK 1.5

<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>

Yennick Trevels said...

Thanks a lot for this excellent tutorial! This is how the Spring Security ACL documentation should've been. Thanks to this article I understand how the pieces come together.

Anonymous said...

Excellent! Keep it up!

Omar Diego said...

Hi there!

It is really a great job, I was wondering how to do that in Spring Security. By the way, have you tried to do the same using JSecurity or another framework? Could it be easier using another solution?

-Omar Diego

Grzegorz Borkowski said...

@Omar:
I planned to do the same thing with JSecurity (currently: Apache Ki); I've started learning JSecurity, but at that time it was not very well documented (though JavaDocs were really good). The API looked easier and more natural than in Spring Security, but I was a bit disappointed with lack of any manual, tutorials etc. Unfortunatelly, I never had time to continue with this job, so it was abandoned.

Michael said...

Grzegorz,
Great explanation and great document!
Thanks!

Any case, I do not know how to implement the following use case.
For example, we have in our organization a director and the director can accept reports of any employer.

How can we implement this without adding explicitly 4 entries in the acls map?
How can we add in the acls map some “ALL” entry?

Do you think it is common use case when some identity should have some permission for all domain objects (its mean all domain objects belong to the identity).

Thanks and best regards,
Michael

Grzegorz Borkowski said...

@Michael,
If the permission is for all objects, than probably there is no need for ACLs: simply use RoleVoter. If it related to specific objects, you probably need some kind of cumulative permission. Look at Spring Security API, there is a class CumulativePermission there. I've never used it so I can't say much, but perhaps it's what you need.

Michael said...

Grzegorz,
Thanks for the fastest answer!
Probably RoleVoter will answer my requirements.

I have more questions :)
1) Do you have example of ACL with ownership?
2) Why do you think Spring Security goes to the implementation of voter per ACL?
I mean why it is not possible to develop one voter that will be configured by ACL – Permissions map and will perform all ACL staff?
I think the configuration will be simpler and the performance will be better.
3) (Not all related to ACL)
In Spring Security 2.0 were promoted the following features: hierarchical roles and a user management API.
Any case, according to forums nobody success to use hierarchical roles.
Did you try the feature?
Regarding user management API: I din to find any reference to this feature in the documentation and code.
Do you know what this feature mean? Do you know what classes should be used?


Thanks and best regards,
Michael

Grzegorz Borkowski said...

Michael,
1) No, I've never used ACL with ownership. I guess it may be useful in case of using mutable ACL service, than for example you can change ACLs which are "your own ones" only.
2) I have no idea about reasons why it was implemented like it was, and whether it could have been done the way you propose, you must ask Spring Security authors :)
3) As far as I remember, the Denksoft tutorial for ACLs uses the concept of hierarchical roles. You may check if it works there. And regarding documentation of many features in Spring Secuiry: I also have many reservations about it, too many things are not clear enough. But it was reported many times on jira and on forums that this framework should be much better documented because of its high complexity. We'll see what version 3.0 brings into this area...

Peter said...

Great tutorial
but i have one practical question
Is it way to get programmatically all ObjectIdentities (certain type of class - ACL_CLASS) that some Authentication has for example Permission "read" ?
Imagine situation I want list all Documents that logged user have access to read

Best regards
Peter

kokaku said...

It's possible this is due to a change in Spring Security 3.x, but the ObjectIdentity iface uses type:String, not javaType:Class. That gives a bit more flexibility when it comes to the data model.

Jakob said...

Great work!

I#m currently having trouble with a Roo-App using the spring ACL.

I saw your comment on http://jira.springframework.org/browse/SEC-1232 did you get it to work within a Roo-WebApplication?

cheers

Grzegorz Borkowski said...

@Jakob:
No, I haven't much to do with Roo yet.

Unknown said...

Very good article, thank so much for sharing this. Vishwa

Unknown said...

Hi

You are suggesting that assigning ACL to the Report is impractical, but this is what is required in most of the production applications. You don’t assign an ACL to user rather you would assign a ACL onto the object.

Did you made that suggestion because it's a POC or is it what you think is the best for Production as well?

I just want to take your opinion as I'm currently developing a Security solution for enterprise applications

Thanks in advance

Vish

Grzegorz Borkowski said...

Vish, I can't help you much with this. The tutorial was written in 2008, almost two years ago, and I don't remember exactly the details of design and code, I would have to spend too much time now trying to analyze it again, and unfortunately at this moment I'm a very very busy man... sorry for this, but I no longer can help much with it.

Unknown said...

Excellent Article...A Great Help regarding gaining a better understanding of Fine Grained Access Control with Spring Security.

Thanks...

Anonymous said...

very useful article, Thnx!

Anonymous said...

Nice example!
I like to port it to use Spring 3. But for now I can't find out how the registration of the custom permission (static block in ExtendedPermission) should look like in spring 3. Any suggestions ?

static {
registerPermissionsFor(ExtendedPermission.class);
}

roolvid said...
This comment has been removed by the author.
roolvid said...

@ R.Domingo

check this http://forum.springsource.org/showthread.php?t=93416

Unknown said...

Exceptionally Superb...Thank you So much Grzegorz Borkowski. I was struggling with the Springs Documentation and also with the only post @ Denksoft Blog. This Article is truly superb and well explained.

Unknown said...

Exceptionally Superb work...Thank you So much Grzegorz Borkowski. I was struggling with the Springs Documentation and also with the only post @ Denksoft Blog. This Article is truly superb and well explained.

hniso said...

Great tutorial
Thakn you.

davidof said...

I've made the updates to Spring 3.0, principally involving the ExtendedPermission. I've posted some more notes and the code here.

http://www.abcseo.com/tech/java/spring-security-3.0-acl-tutorial

great work on this tutorial!

Pradeep Kolakala said...

Thank u sooooooooooooo much. Its helped me a lot. I was scared when I got chance to work with ACL. But now I got good idea. I am really thankful to you.

Williams said...

excellent, I'm going to implement this. Greetings from Venezuela

Anonymous said...

Hi,

Your example is great and works fine for me. I just have a question.
I have a complex application with different modules (news, group, blog, announcements, post). Inside each module there are different roles and the client wants the application to be flexible enough to define new roles at anytime. Of course there is a hierarchy of roles (like a tree) and a user with a role in an upper level also has the roles defined in the corresponding lower levels of the role (in this case a node with children nodes).
Everything seems pretty simple till now but the client also wants that a module will communicate with other modules and that for example a group which was given a specific set of rights in the blog module, gets another set of rights in the announcement module.
There is also the requirement that the news module should collect all data from all the other modules and display the information according to each users rights (only if the user has the permission to see the data from other modules should it be displayed). So it's a pretty complex user-rights-management model.
My question is can Spring ACL help me with what I need to do? Do you know of any other more appropriate solutions? Could you maybe direct me in some way or another (useful links, posts)?

my email is jucan@lyth.de

thanks

alfaomega83 said...

Hi!thank you for you nice tutorial!
I want to adapt for mine application...At the start we have to call an ejb to retrieve the list of roles for the speficic user. After we set the permission(like : read,write ecc) in a map in session.
Is possible?

Thread in Java said...

Great post and thanks covering in detail with example. I also found How to do LDAP authentication in Active directory using spring security very helpful and worth sharing. you may also like it.

Anupam said...

Excellent tutorial. Thanks !

Anonymous said...

i have the acl authrization like this in the xml.

















How can i get the roles from the db Instead of ROLE_ADMIN

Erhan Kesken said...

i think this tutorial must be spring security acl's official tutorial.

i opened a repository for 3.1 version at github:

https://github.com/ekesken/springacltutorial

i'm planning to add new proof of concepts (for other security annotations such as @Pre, @Post, @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter)

everyone is welcome to add their pocs.

laxman said...

Can any one please help me...how to provide authorization in wicket and spring application...

Gal Levinsky said...

Wanted to join and agree with the other comments - EXCELLENT post!

Should be part of the official documentation.

Anonymous said...

Excellent Work

chivivoto said...

Another tutorial about Spring Security ACL

http://www.proyectosbds.com/blog/framework-spring-v-3-1-1-spring-security-v-3-1-0-acl-mysqlconnector-bd/#more-417

Unknown said...

Excellent post....!!!

THANKS..!!!

Anonymous said...

great work thanks alot

Ashish said...

The best post on the topic. I have gone through many of the blogs but it gives me a feel that the topic is not very clear to the Author itself. But you have done excellent research before writing this blog. Hats off dear