Monday, October 31, 2005

ACEGI Tutorial: An Example of Method-based Access Control and JUnit for Testing

NOTE: Updated 2006, May 10 - changed net.sf to org in package names. And updated code to work with ACEGI v1.0 RC2

This ACEGI tutorial shows how to implement the following security requirements for the BookBean class:

  • Only MANAGER users can replace the existing value (ie, call setValue)
  • Only MANAGER users and WORKER users can change the value (ie, call changeValue)
  • Any user (ie, someone with no roles) can view the value (ie, call getValue)

The BookBean class is extremely simple as shown by its interface:

/* BookBean.java */
package com.affy;

public interface BookBean {
    public int getValue();
    public void setValue(int _value);
    public void changeValue(int _value);
}

Likewise, the implementation of this interface is simple:

/* BookBeanImpl.java */
package com.affy;

public class BookBeanImpl implements BookBean {

    private int value = 0;

    public BookBeanImpl() {
        super();
    }

    public int getValue() {
        return this.value;
    }

    // replace the value.
    public void setValue(int _value) {
        this.value = _value;
    }

    // change the value.
    public void changeValue(int _value) {
        this.value += _value;
    }

}

Now that we've seen the interface and the implementation, we can look at how a Spring configuration file is used to provide declarative access control. I'll note at this point that while the user information (ie, names, passwords, etc...) is hard-coded in the configuration file you can also use a database, ldap, or whatever persistent storage mechanisn you prefer.

I find it hard to glean useful information when I read XML configuration files; I suspect it's a learned skill. However, the configuration file for this example is only 56 lines of code - much of which can be considered boilerplate.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>

<-- spring.xml -->

  <-- This is the bean that needs to be protected. -->
  <bean id='bookBean' class='com.affy.BookBeanImpl'/>

  <-- This bean defines a proxy for the protected bean. Notice that -->
  <-- the id defined above is specified. When an application asks Spring -->
  <-- for a bookBean it will get this proxy instead. -->
  <bean id='autoProxyCreator' class='org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator'>
    <property name='interceptorNames'>
      <list><value>securityInterceptor</value></list>
    </property>
    <property name='beanNames'>
      <list><value>bookBean</value></list>
    </property>
  </bean>

  <-- This bean specifies which roles are authorized to execute which methods. -->
  <bean id='securityInterceptor' class='org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor'>
    <property name='authenticationManager' ref='authenticationManager'/>
    <property name='accessDecisionManager' ref='accessDecisionManager'/>
    <property name='objectDefinitionSource'>
      <value>
        com.affy.BookBean.setValue=ROLE_MANAGER
        com.affy.BookBean.changeValue=ROLE_WORKER,ROLE_MANAGER
      </value>
    </property>
  </bean>

  <-- This bean specifies which roles are assigned to each user. You'll notice  -->
  <-- that I'm using an in-memory database implementation instead of using  -->
  <-- LDAP or a 'real' database. The ACEGI-provided in-memory implementation is great for testing! -->
  <bean id='userDetailsService' class='org.acegisecurity.userdetails.memory.InMemoryDaoImpl'>
    <property name='userMap'>
      <value>
        manager=manager,ROLE_MANAGER
        worker=worker,ROLE_WORKER
        anonymous=anonymous,
        disabled=disabled,disabled,ROLE_WORKER
      </value>
    </property>
  </bean>
 
  <-- This bean specifies that a user can access the protected methods -->
  <-- if they have any one of the roles specified in the objectDefinitionSource above. -->
  <bean id='accessDecisionManager' class='org.acegisecurity.vote.AffirmativeBased'>
    <property name='decisionVoters'>
      <list><ref bean='roleVoter'/></list>
    </property>
  </bean>

  <-- The next three beans are boilerplate. They should be the same for nearly all applications. -->
  <bean id='authenticationManager' class='org.acegisecurity.providers.ProviderManager'>
    <property name='providers'>
      <list><ref bean='authenticationProvider'/></list>
    </property>
  </bean>

  <bean id='authenticationProvider' class='org.acegisecurity.providers.dao.DaoAuthenticationProvider'>
    <property name='userDetailsService' ref='userDetailsService'/>
  </bean>

  <bean id='roleVoter' class='org.acegisecurity.vote.RoleVoter'/>
   
</beans>

So we've seen the interface, the implementation, and the Spring configuration file. The last file shows how to unit test the access control. One important thing to note is that the SecuriyContextHolder is a static object. So while the approach shown below is valid for an single-thread application which can change its user context as needed, there are definitely some synchronization issues that need to be addressed for multi-threaded applications.

// MethodAclTest.java
package com.affy;
 
import junit.framework.TestCase;
import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.Authentication;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.DisabledException;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.context.SecurityContextImpl;
import org.acegisecurity.providers.AuthenticationProvider;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MethodAclTest extends TestCase {

    // Read the Spring configuration file. I typically create a directiory called config which
    // gets added to my classpath.
    private static ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");

    private static void createSecureContext(final ApplicationContext ctx, final String username, final String password) {
        AuthenticationProvider provider = (AuthenticationProvider) ctx.getBean("authenticationProvider");
        Authentication auth = provider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        SecurityContextHolder.getContext().setAuthentication(auth);
    }

    // Clear the security context after each test.
    public void teardown() {
        SecurityContextHolder.setContext(new SecurityContextImpl());
    }

    // There are three methods with three roles Which means that I need
    // nine tests.

    ///////////////////
    // setValue tests.
    ///////////////////

    public void testManagerAccessForSet() {
        createSecureContext(ctx, "manager", "manager");
        ((BookBean) ctx.getBean("bookBean")).setValue(100);
    }

    public void testWorkerAccessForSet() {
        createSecureContext(ctx, "worker", "worker");
        try {
            ((BookBean) ctx.getBean("bookBean")).setValue(100);
            fail("Expected AccessDeniedException.");
        } catch (AccessDeniedException e) {
            // do nothing.
        }
    }

    public void testAnonymousAccessForSet() {
        createSecureContext(ctx, "anonymous", "anonymous");
        try {
            ((BookBean) ctx.getBean("bookBean")).setValue(100);
            fail("Expected AccessDeniedException.");
        } catch (AccessDeniedException e) {
            // do nothing.
        }
    }

    ///////////////////
    // changeValue tests.
    ///////////////////
    public void testManagerAccessForChange() {
        createSecureContext(ctx, "manager", "manager");
        ((BookBean) ctx.getBean("bookBean")).changeValue(100);
    }

    public void testWorkerAccessForChange() {
        createSecureContext(ctx, "worker", "worker");
        ((BookBean) ctx.getBean("bookBean")).changeValue(100);
    }

    public void testAnonymousAccessForChange() {
        createSecureContext(ctx, "anonymous", "anonymous");
        try {
            ((BookBean) ctx.getBean("bookBean")).changeValue(100);
            fail("Expected AccessDeniedException.");
        } catch (AccessDeniedException e) {
            // do nothing.
        }
    }
    
    ///////////////////
    // getValue tests.
    ///////////////////
    public void testManagerAccessForGet() {
        createSecureContext(ctx, "manager", "manager");
        ((BookBean) ctx.getBean("bookBean")).getValue();
    }

    public void testWorkerAccessForGet() {
        createSecureContext(ctx, "worker", "worker");
        ((BookBean) ctx.getBean("bookBean")).getValue();
    }

    public void testAnonymousAccessForGet() {
        createSecureContext(ctx, "anonymous", "anonymous");
        ((BookBean) ctx.getBean("bookBean")).getValue();
    }

    ///////////////////
    // disabled user
    ///////////////////
    public void testDisabledUser() {
        try {
            createSecureContext(ctx, "disabled", "disabled");
            fail("Expected DisabledException.");
        } catch (DisabledException e) {
            // do nothing.
        }
    }

    ///////////////////
    // unknown user
    ///////////////////
    public void testUnknownUser() {
        try {
            createSecureContext(ctx, "unknown", "unknown");
            fail("Expected BadCredentialsException.");
        } catch (BadCredentialsException e) {
            // do nothing.
        }
    }

}

In conclusion, once I created my own example the ideas and classes used by ACEGI became quite clear and easy to use. I will definitely look to ACEGI in future applications.

Post a Comment