This tutorial shows the basic steps for setting up the SAF Core module in Spring applications. It shows how to configure the module within a Spring application context XML file and how to apply security annotations to domain objects and Spring beans. The sample application implements an AccessManager with over-simplified, static authorization decision rules that do not consider users or roles. For setting up an application with the SAF JAAS authorization provider refer to the Notebook sample.
To run the sample application download first the latest SAF source release and build it with Maven 2. You should now be in the project's root directory (safr-<version> where <version> is the version number of the source release you downloaded). Then go to the safr-sample-hellosaf sub-directory and run the command
mvn test
This will run the sample application.
We start by defining a sample domain object (DomainObject) for which permission checks shall be enforced. The domain object defines an id attribute which can be set at construction time. Authorization decisions will be based on that id.
@SecureObject public class DomainObject { private long id; public DomainObject(long id) { this.id = id; } public long getId() { return id; } @Secure(SecureAction.UPDATE) public void update() { // update (modify) this domain object ... } ... }
Since domain objects aren’t managed by a Spring application context we add a @SecureObject annotation on class-level. Only classes with @SecureObject annotations are weaved by the AspectJ compiler. The domain object defines an update() method that is annotated with @Secure(SecureAction.UPDATE). When we invoke this method on a domain object instance then the SAF calls AccessManager.checkUpdate() passing that instance as argument to checkUpdate().
In a next step we define a service interface and an implementation class with methods that operate on domain objects. findDomainObject() and findDomainObjects() return domain objects, deleteDomainObject() has a DomainObject parameter. For the first two methods we want to check whether a caller has read permissions for the returned domain objects. Therefore, a @Filter annotation is added.
public interface Service { @Filter DomainObject findDomainObject(long id); @Filter List<DomainObject> findDomainObjects(long... ids); void deleteDomainObject(@Secure(SecureAction.DELETE)DomainObject obj); }
The @org.springframework.stereotype.Service annotation of ServiceImpl is part of the Spring Framework since version 2.5. We use the annotation to load the service bean from the classpath (for more details refer to the SAF home page).
package net.sourceforge.safr.sample.hellosaf; import java.util.ArrayList; import java.util.List; @org.springframework.stereotype.Service public class ServiceImpl implements Service { public DomainObject findDomainObject(long id) { return new DomainObject(id); } public List<DomainObject> findDomainObjects(long... ids) { ArrayList<DomainObject> result = new ArrayList<DomainObject>(); for (long id : ids) { result.add(new DomainObject(id)); } return result; } public void deleteDomainObject(DomainObject obj) { // delete domain object (from database) ... } }
For findObject() the SAF calls AccessManager.checkRead(). It passes the object returned by findObject() as argument to checkRead(). If checkRead() throws an AccessControlException null will be returned from a findObject() call otherwise a domain object is returned. For findDomainObjects() the SAF calls AccessManager.checkRead() for every domain object in the result list. If checkRead() throws an AccessControlException the SAF will not add the affected object to the result. For deleteDomainObject() we want to check whether the caller has permissions to delete the domain object instance that is passed as argument to this method. When the method is invoked the SAF calls AccessManager.checkDelete(). It passes the deleteDomainObject() call-argument as argument to checkDelete(). An AccessControlException thrown by checkDelete() will not be handled by the SAF; this must be done by the caller. The SAF only handles AccessControlExceptions with @Filter annotations.
Let’s look at the AccessManagerImpl class. It implements authorization decision logic that only allows create, read, update and delete actions on domain objects with ids less than 10. For all other ids AccessControlExceptions will be thrown. There's also a type-level annotation @PolicyDecisionPoint. If you use the component-scanning feature of Spring 2.5 (or higher) classes annotated with @PolicyDecisionPoint are loaded into the Spring application context as bean with name "accessManager" (default). The bean name can be customized by defining an annotation value. For example, @PolicyDecisionPoint("myBean") creates a bean with name "myBean".
package net.sourceforge.safr.sample.hellosaf; import java.security.AccessControlException; import net.sourceforge.safr.core.invocation.MethodInvocation; import net.sourceforge.safr.core.invocation.ProceedingInvocation; import net.sourceforge.safr.core.provider.AccessManager; import net.sourceforge.safr.core.spring.annotation.PolicyDecisionPoint; /** * @author Martin Krasser */ @PolicyDecisionPoint public class AccessManagerImpl implements AccessManager { public void checkCreate(Object obj) { checkObject(obj); } public void checkRead(Object obj) { checkObject(obj); } public void checkUpdate(Object obj) { checkObject(obj); } public void checkDelete(Object obj) { checkObject(obj); } ... private void checkObject(Object obj) { DomainObject domainObject = (DomainObject)obj; if (domainObject.getId() > 9) { throw new AccessControlException("access to domain object with id " + domainObject.getId() + " denied"); } } }
In the Spring application context XML file, we activate the SAF Core module with the <sec:annotation-driven> element. The access-manager attribute references the accessManager bean. We also define ServiceImpl as a bean.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://safr.sourceforge.net/schema/core" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://safr.sourceforge.net/schema/core http://safr.sourceforge.net/schema/core/spring-safr-core-1.0.xsd"> <sec:annotation-driven access-manager="accessManager" /> <bean id="accessManager" class="net.sourceforge.safr.sample.hellosaf.AccessManagerImpl" /> <bean id="service" class="net.sourceforge.safr.sample.hellosaf.ServiceImpl" /> </beans>
You may also want to use the new Spring features available since version 2.5 to configure the SAF. The following application context XML file loads all beans from the classpath starting at the package root net.sourceforge.safr.sample.hellosaf. In our example, this will load AccessManagerImpl (annotated with @PolicyDecisionPoint) and ServiceImpl (annotated with @Service). The access-manager attribute of <sec:annotation-driven> is not defined because we're using the default bean name for the access manager bean.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ctx="http://www.springframework.org/schema/context" xmlns:sec="http://safr.sourceforge.net/schema/core" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://safr.sourceforge.net/schema/core http://safr.sourceforge.net/schema/core/spring-safr-core-1.0.xsd"> <ctx:component-scan base-package="net.sourceforge.safr.sample.hellosaf"/> <sec:annotation-driven /> </beans>
In the pom.xml add a dependency to the safr-core-<version>.jar jar file where <version> is the version number of the source release you downloaded. In this example it is 0.9.
<dependencies> ... <dependency> <groupId>net.sourceforge.safr</groupId> <artifactId>safr-core</artifactId> <version>0.9</version> </dependency> </dependencies>
Before we can test our sample application we have to tell the AspectJ compiler to use the safr-core-0.9.jar file as aspect library. With the AspectJ Maven 2 plugin this can be done via the following configuration in the pom.xml file.
<build> <plugins> ... <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <configuration> ... <aspectLibraries> <aspectLibrary> <groupId>net.sourceforge.safr</groupId> <artifactId>safr-core</artifactId> </aspectLibrary> </aspectLibraries> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
To use AspectJ directly in Eclipse you must install the AspectJ Development Tools (AJDT). After installation, convert the Eclipse project you are working on to an AspectJ project and open the project properties. Under the AspectJ Build menu add the safr-core-0.9.jar file to the Aspect Path. The classpath variable M2_REPO refers to the local Maven 2 repository
Finally, here’s the Junit4 test code of our sample application. Spring's new test framework is used that is available since version 2.5.
package net.sourceforge.safr.sample.hellosaf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.security.AccessControlException; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations={"/context.xml"}) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class}) public class SampleTest { @Autowired private Service service; @Test public void testFindDomainObject() { DomainObject result = service.findDomainObject(1); assertNotNull(result); result = service.findDomainObject(11); assertNull(result); } @Test public void testFindDomainObjects() { List<DomainObject> result = service.findDomainObjects(2, 3, 4); assertEquals(3, result.size()); result = service.findDomainObjects(2, 3, 14); assertEquals(2, result.size()); assertTrue(result.contains(new DomainObject(2))); assertTrue(result.contains(new DomainObject(3))); result = service.findDomainObjects(12, 13, 14); assertEquals(0, result.size()); } @Test public void testDeleteDomainObject() { service.deleteDomainObject(new DomainObject(5)); try { service.deleteDomainObject(new DomainObject(15)); fail(); } catch (AccessControlException e) { // expected } } @Test public void testUpdate() { new DomainObject(6).update(); try { new DomainObject(16).update(); fail(); } catch (AccessControlException e) { // expected } } }