Acegi - the Java security framework
I suppose that you know what is Spring Security (former Acegi project) is, or at least you are aware of its wide securing abilities for a Java web application. Besides lots of features which Acegi offers you out-of-the box, there are many configurable options. This time, I had a task to implement a functionality which allows restricting access to method invocations based not only on the granted roles to the logged in user, but taking in account passed parameters as well. Let’s see the result.
@Secured annotations
Assume that you are developing pretty common web application with standard security model with users, granted roles, etc. Of course, the main goal of the security infrastructure is accepting/denying user access to different parts of your application based on the granted access rights. Acegi offers you two main ways of controlling access: URL based which analyzes the clients http requests to your application and based on the AOP principles which controls method invocations.
With the URL based method you can achieve rough securing coverage very simply. For instance, if you have /admin part, you just deny the access to all users who don’t have ROLE_ADMIN granted role to the URLs which starts from /admin/** string. The part /store you can make available only to users with role ROLE_CUSTOMER, etc. You can cover all you URLs with such rules, but unfortunately, if you want more precise control, you have to write lots of business logic code in your Controllers/Managers which performs additional checks in the methods body.
Let’s consider an idiomatic example (no real DAOs, no Hibernate etc) consisting of:
- a controller ShoppingCartAddItemController which is mapped to /shop/card/addItem.jsp url,
- a service class IShoppingCartManager which provides with business logic,
- ROLE_USER which is granted to Scott user:
public class ShoppingCartAddItemController extends AbstractController {
private IShoppingCartManager shoppingCartManager;
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request,
HttpServletResponse response) throws Exception {
Integer customerId = ServletRequestUtils.getIntParameter(request, "customerId");
Integer itemId = ServletRequestUtils.getIntParameter(request, "itemId");
Integer amount = ServletRequestUtils.getIntParameter(request, "amount");
shoppingCartManager.addItem(customerId, itemId, amount);
return new ModelAndView("redirect:/shop/item/congratulations.jsp");
}
public void setShoppingCartManager(IShoppingCartManager shoppingCartManager) {
this.shoppingCartManager = shoppingCartManager;
}
}
public interface IShoppingCartManager {
@Secured({"ROLE_USER" })
void addItem(Integer customerId, Integer itemId, Integer amount);
@Secured({"ROLE_USER" })
void deleteItem(Integer customerId, Integer itemId);
}
public class ShoppingCartManager implements IShoppingCartManager {
public void addItem(Integer customerId, Integer itemId, Integer amount) {
// use DAO
}
public void deleteItem(Integer customerId, Integer itemId) {
// use DAO
}
}
The ROLE_USER guarantees that the secured method can be invoked from the controller if the logged in user is granted by this role. But in this case, being logged in, I can change the URL parameter customerId to another value and Acegi won’t check it. To prevent it, we need to paste something like this in every Manager method (or Controller or Filter for every URL which matches /shop/**/*?customerId=… pattern):
public void addItem(Integer customerId, Integer itemId, Integer amount) {
Integer loggedCustomerId = securityManager.getLoggedCustomerId();
if (loggedCustomerId != customerId) {
throw new AccessDeniedException("access denied");
}
// business logic
}
The main question is: how can we minimize coding, if we know that lots of methods requires the same type of access check? The answer is pretty obvious - we need a AOP aspect which would do this routine instead of us. From another point of view, it would be great if we could employ already used @Secured annotation facilities.
Conditional Roles
After brief googling, I caught the first clue. It was a similar question on Spring framework forum, and eventually I came to the topic which lead to Conditional Roles patch SEC-273. It was an implementation by Usama Rashwan of the idea of Karl Moore.
The idea is elegant and straightforward. All tricks with method invocations in Acegi could be done by implementing RoleVoter interface where you can implement your own access decision logic. But writing your own RoleVoter for every scenario is violating DRY principle. So, it was suggested to write one RoleVoter and to use a Scripting Language like OGNL, MVEL to write conditions right in the role names after special delimiter “::”.
In our case, the new @Secured annotation changes to:
@Secured({"ROLE_USER::authentication.principal.customerId == arg0" })
void addItem(Integer customerId, Integer itemId, Integer amount);
where authentication is SecurityContextHolder.getContext().getAuthentication() object which usually holds all logged in user details. This object is put to MVEL context by AbstractInvocationChecker. Besides this object, you can operate by arguments passed to the secured method, here arg0 is the first parameter passed to the method. The MVEL scripting language is very flexible, it can cope with JavaBeans, operations on collections, etc. Of course you can’t do there rocket science computations, but for our needs it is enough.
I suggest the next way of usage:
- Extend UserDetails class with information required in decision point when the secured method is invoked. It could be ids of objects which is allowed to be manipulated by logged in user.
- Populate UserDetails with all needed information while a user logging in by UserDetailsService. This object is available as “authentication.principal” object in MVEL context.
- Write conditional roles based on this objects mixing with passed arguments.
- Update the SecurityContextHolder.getContext().getAuthentication() if necessary during the user activity.
Buzz
If you are interested in this topic - just download the patch, it has a comprehensive example of usage and clear source code. The JIRA issue stated that this patch goes to 2.0.0 M2 which is going to be released soon.


