In Bamboo 1.2, we introduced plan level permissions as a major feature. Already with an Acegi Security framework in place, we figured it was a natural extension to build our permissions framework on top of Acegi.
Bamboo Security Architecture
There are really two sides to security in Bamboo (or any other application for that matter):
- Authentication – verifying that the user is valid
- Authorization – verifying that the user has the appropriate rights to perform an action (which includes accessing some information).
Below is an overview of the security framework implemented in Bamboo. The focus of the discussion will be on the authorization aspects and how we have extended the framework for our purposes in Bamboo.
Acegi provides both authentication and authorization services. In Bamboo, we have predominantly kept the authentication services provided by Atlassian Seraph, while delegating the authorization work to Acegi. A filter (SeraphLoginFilter) has been put in place of Acegi’s default authentication filters to put the user into Acegi’s SecurityContextHolder.
There are many “places” in which authorization is implemented in Bamboo. By default, Acegi provides two forms of authorization:
- Web Request Authorization
- AOP Security Interceptor Authorization
In addition, we have implemented our own custom authorization on a XWork interceptor level.
1. Web Request Authorization
Implemented as a servlet filter FilterInvocationInterceptor, this can determine whether the logged in user has the appropriate authority to go to a URL by way of URL pattern matching for authority. e.g.
/viewadministrators.action=ROLE_ANONYMOUS,ROLE_USER /admin/**=ROLE_ADMIN /build/admin/**=ROLE_ADMIN /**=ROLE_USER
However, this form of authorization will not suffice, as we need to authorize on a more granular level - based on plans.
2. AOP Security Interceptor Authorization
Implemented as a Spring AOP Interceptor MethodSecurityInterceptor, this form of authorization can provide object level (or in Bamboo's case, plan level) permissions (i.e. Bob can only kick off a build on plan BAM-MAIN). The interceptor wraps around manager level methods (or any other object in Spring for that matter) and filters on the specified method invocations.
The interceptor can provide authorization either:
- Before Invocation - in this case, the AccessDecisionManager will assess whether the function in the bean is allowed to be called by the principal in context. The AccessDecisionManager contains a set of Voter objects, which will vote on whether the principal in context should be allowed access.
- After Invocation - in this case, the AfterInvocationManager will look at the returned object from the function, and determine whether the principal in context should have access. In the case of a Collection of objects returned, the AfterInvocationManager can filter out those in the collection to which the principal should not have access. The AfterInvocationManager contains a set of AfterInvocationProvider objects, which will either grant (deny) access to the returned object, or filter a returned collection.
Below is the extract of the interceptor definition on Bamboo's BuildManager class:
<bean id="permissionsInterceptor" class="com.atlassian.bamboo.security.acegi.intercept.aopalliance.MethodSecurityInterceptor"> <property name="authenticationManager"> <ref bean="authenticationManager"/> </property> <property name="accessDecisionManager"> <ref local="businessAccessDecisionManager"/> </property> <property name="afterInvocationManager"> <ref local="afterInvocationManager"/> </property> <property name="objectDefinitionSource"> <value> <!--Getter methods - protected with AFTER invocation providers for filtering -->
<!--Mutator methods - protected with PRE invocation providers-->
</value> </property> </bean>
The objectDefinitionSource in the extract above is the definition of which interface methods on the BuildManager should be protected, and how. Each line (property) in the objectDefinitionSource represents one method to be protected, and contains a set of ConfigurationAttributes (e.g. ACL_BUILD_EDIT, AFTER_ACL_COLLECTION_READ). Each of these ConfigurationAttributes uniquely map to either a Voter or AfterInvocationProvider, and marks that any invocation of the method should be passed through that Voter or AfterInvocationProvider.
3. XWork Interceptor Authorization
While the web request filter approach failed to provide object level authorization, we found that the AOP invocation authorization had its limitations too. Because they were wrapped around manager level classes, the authorization "rules" were removed from the functional use-case. Ideally, we would like to have authorization at the XWork action level. An XWork Interceptor was the perfect candidate for this. To this end, we extended on the Acegi framework with a new WebworkSecurityInterceptor, which extended Acegi's abstract AbstractSecurityInteceptor class and implemented the XWork Interceptor interface. Thanks to Acegi's customizability, this was a breeze. We then can use the AccessDecisionManager to decide whether an ActionInvcation can go ahead or not (an AfterInvocationManager was not really necessary).
The next step was to put in place a mechanism for determining how each action was to be protected (e.g. the ConfigureBuild action should only be accessible by principals who have EDIT permission on the build in that action). Each action that needs to be protected implements the DomainObjectSecurityAware interface, which has only one method:
public interface DomainObjectSecurityAware
The getSecuredDomainObject returns the object that needs to be protected. In the case of ConfigureBuild, this will be the Build object. Our Awareness interceptor stack ensures that the Build object would be populated in our action before the WebworkSecurityInterceptor fires.
While the DomainObjectSecurityAware interface defines what gets protected, we also need to define how that object gets protected. For this, we define a set of marker interfaces which extend the generic DomainObjectSecurityAware (e.g. BuildEditSecurityAware). Each action will then simply need to implement one of these marker interfaces.
The last step is to link up each of the "marker" interfaces back to the Voter objects we have defined in our WebworkSecurityInterceptor's AccessDecisionManager. We do this again using an ObjectDefinitionSource:
<property name="objectDefinitionSource"> <value>
Note that in this implementation, the order of the definitions play an important part, as an Action can implement more than one of the marker security interfaces (pretty much all actions implement GlobalReadSecurityAware).
Domain Security Object Model (ACL)
The domain security object model defines the storage of permissions against the domain objects which need authorization. The Acegi package comes with a predefined model in the Acl (Access Control List) module. The module provides an interface to storing and retrieving information about how a domain object will be authorized (i.e. which principal, group, or authority will have permission to access which object with which permissions). However, the Acegi implementation is based on a direct JDBC approach which differs from Bamboo's Hibernate persistence layer. To integrate the Acegi framework, we implemented a Hibernate backed implementation of the Acl module.
In Bamboo, there are two types of objects which will be protected:
- Build (com.atlassian.bamboo.build.Build) - i.e. the plan.
- GlobalApplicationSecureObject (com.atlassian.bamboo.security.GlobalApplicationSecureObject) - a singleton object representing global level actions.
For authorization, each object that needs to be protected will have a corresponding Acl object (i.e. there will be one Acl for the GlobalApplicationSecureObject, and one Acl for each build plan in Bamboo). Each Acl can have multiple AccessControlEntries (ACE), which define what a particular Sid (Security Id) can access by way of a permission mask. In Bamboo a Sid can translate to either: a user, a group, or a granted authority (e.g. anonymous users or logged in users).
All in all, the implementation of the Acegi security framework and extending it was one relatively smooth ride for the Bamboons. What really rocked for us was the neatness with which things were done, and the complete customizability the framework affords.