How do YOU test access control of your application?

Many of complex applications put on top of their complexity access control logic for securing data and to limit access to certain functions. No matter if you have fully configurable ACL settings based on rights or role based access you'd probably want to test this part of application too. In order to have proper test coverage you should make it easy for you and your colleagues to test this. I have no doubts that if you ever needed to test this you already have some kind of such test support, but this article describes what kind of it I've created for myself. It might be interesting for you to compare it with your solution or inspire you to create one if you haven't done it already.

Let's state some starting points. We use Spring Framework test framework in combination with JUnit 4 and Spring Security method level EL based security rules. Our business methods look like this:


@AllowedForAdministrator
@AllowedForTerminalOrganizationOwner
@DeniedForMerchant
public void blockTerminal(Terminal terminal) {
   //business logic
}

Note: If you wonder what @Allowed* annotations mean you might want to read another my article Custom annotations for Spring Security - but in short you can imagine them as multiple @PreAuthorize annotations with some SpEL in them.

In order to test upper mentioned method properly you should test at least following scenarios:

  • call method as administrator - should be allowed
  • call method as organization owner - should be allowed
  • call method as merchant - should throw AccessDeniedException
  • call method as unauthorized user - should throw AccessDeniedException

You need to login proper user before test run and log him out on tear down. This could look very ugly because for each of such test you need different user to be logged in so you cannot take advantage of @Before or @BeforeClass annotations.

Extend your test execution lifecycle

In Spring test support you can use so called TestExecutionListener that will be called by framework before / after executing each of the test method. Each callback you can override has access to the TestContext object with reference to the reflection object of the test method and other useful things. Having known that, we could create our own listener that would examine annotations on test method and will take care of logging in a new user and safely cleaning after the test. See example of the test:


@Test
@RunAsUser("owner@fg.cz")
public void shouldBlockTerminalAsOrganizationOwner() throws Exception {
   Terminal terminal = terminalManager.getTerminalById(100);
   assertNull(terminal.getDateBlocked());
   terminalManager.blockTerminal(terminal);
   terminal = terminalManager.getTerminalById(100);
   assertNotNull(terminal.getDateBlocked());
}
@Test
@RunAsUser("administrator@fg.cz")
public void shouldBlockTerminalAsAdministrator() throws Exception {
   Terminal terminal = terminalManager.getTerminalById(100);
   terminalManager.blockTerminal(terminal);
}
@Test(expected = AccessDeniedException.class)
@RunAsUser("merchant@fg.cz")
public void shouldFailToBlockTerminalAsMerchant() throws Exception {
   Terminal terminal = terminalManager.getTerminalById(100);
   terminalManager.blockTerminal(terminal);
}
@Test(expected = AccessDeniedException.class)
public void shouldFailToBlockTerminalAsUnauthorized() throws Exception {
   Terminal terminal = terminalManager.getTerminalById(100);
   terminalManager.blockTerminal(terminal);
}

Now you got the point - as you can see testing access rights in such way is really easy so you can provide sufficient security coverage. I've tried this approach on a year long project consisting of 60k+ lines of code and have had really great experience with it. Now how you to achieve this test behaviour:

Define custom annotation ...

... that you'd place on test methods and would mark them to be enveloped by the "logging in" logic:


/**
 * Allows to run test in the context of logged in frontend user.
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RunAsUser {
   /**
     * Method returns login of logged in user.
     * @return
     */
    String value();
}

Write custom TestExecutionListener ...

... that would process aforementioned annotation. You'd probably want to extend existing AbstractTestExecutionListener that allows you to implement only those callback you really want to override.


/**
 * Supports annotations {@link RunAsUser}.
 */
public class RunAsSupportTestExecutionListener extends AbstractTestExecutionListener {
    private static final ThreadLocal<Authentication> savedAuthentication = new ThreadLocal<Authentication>();
    private static final ThreadLocal<User> savedAdmin = new ThreadLocal<User>();
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        super.beforeTestMethod(testContext);
        final RunAsUser runAsUser = testContext.getTestMethod()
                                      .getAnnotation(RunAsUser.class);
        if (runAsUser != null) {
            final String userName = runAsUser.value();
            loginAsUser(
               userName, testContext.getApplicationContext()
            );
        }
    }
    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        super.afterTestMethod(testContext);
        final RunAsUser runAsUser = testContext.getTestMethod()
                                     .getAnnotation(RunAsUser.class);
        if (runAsUser != null) {
            logoutUser();
        }
    }
    public static void loginAsUser(String userName, ApplicationContext appCtx) {
        UserDetailsService userDetailsService = getDaoAuthenticationProvider(appCtx);
        final UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken(
                userDetails, userDetails.getPassword(),
                new ArrayList<GrantedAuthority>(
                    userDetails.getAuthorities()
                )
            )
        );
    }
    private static void logoutUser() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }
    private static UserDetailsService getDaoAuthenticationProvider(ApplicationContext appCtx) {
        UserDetailsService userDetailsService;
        final Map<String,UserDetailsService> userDetailsServiceIndex = appCtx.getBeansOfType(
            UserDetailsService.class
        );
        if (userDetailsServiceIndex.size() == 1) {
            userDetailsService = userDetailsServiceIndex.values()
                                      .iterator().next();
        } else {
            throw new IllegalStateException(
                "Cannot determine user detail service - there is " +
                userDetailsServiceIndex.size()
                + " beans of class UserDetailsService!"
            );
        }
        return userDetailsService;
    }
}

Initialize Test (or better TestAncestor) class with listener ...

... to setup your test class to use newly created test execution listener. You probably already have some test class ancestor - so this is the best place to place this:


@ContextConfiguration(
   locations = {
      "classpath:/META-INF/spring/some-spring-config.xml",
      ... and others ...
   }
)
@TestExecutionListeners( {
   DependencyInjectionTestExecutionListener.class,
   DirtiesContextTestExecutionListener.class,
   RunAsSupportTestExecutionListener.class
})
public abstract class AbstractTest {
   //whatever
}

Finally

Do you like it? Give it +1! I hope it will ease your life when testing access rights - it worked for me it might work for you also. Happy coding!