lunedì 18 luglio 2011

Testing methods secured with Spring Security

Spring Security allows to secure access to methods, allowing execution only for user with defined roles.

For example, using annotation to configure the roles required:

public interface MyService
{

 @PreAuthorize("hasRole('USER') OR hasRole('ADMIN')")
 void hello();

 @PreAuthorize("hasRole('ADMIN')")
 void bye();

}

In order to call the bye() method the user must have the ADMIN role, for hello() method also the the USER role is enough.

If a secured method is called by a user without the needed roles is raised an exception of type org.springframework.security.access.AccessDeniedException.

Method security is active even during tests, so we have developed an annotation and a listener that can be used to setup easily user roles when testing (we use TestNG, probably something like this can be done also with Junit).

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Authenticate {

 String username() default "";

 String[] roles() default {};
}

public class AuthenticationListener implements IInvokedMethodListener
{

 public static final String DEFAULT_USERNAME = "default";

 private static final String FAKE_PASSWORD = "fakePassword";

 /**
  * {@inheritDoc}
  */
 @Override
 public void beforeInvocation(IInvokedMethod method, ITestResult testResult)
 {
  if (method.isTestMethod())
  {
   ITestNGMethod testMethod = method.getTestMethod();
   Method javaMethod = testMethod.getMethod();
   Authenticate userAnnotation = javaMethod.getAnnotation(Authenticate.class);
   if (userAnnotation != null)
   {
    String username = userAnnotation.username();
    if (username == null || username.isEmpty())
    {
     username = DEFAULT_USERNAME;
    }
    String[] roles = userAnnotation.roles(); // role may be null/empty
    authenticateUser(username, roles);
   }
  }
 }

 private void authenticateUser(String username, String... roles)
 {
  Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
  if (roles != null)
  {
   for (String role : roles)
   {
    grantedAuthorities.add(new GrantedAuthorityImpl(role));
   }
  }
  UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, FAKE_PASSWORD,
   grantedAuthorities);
  SecurityContextHolder.getContext().setAuthentication(token);
 }

 /**
  * {@inheritDoc}
  */
 @Override
 public void afterInvocation(IInvokedMethod method, ITestResult testResult)
 {
  if (method.isTestMethod())
  {
   SecurityContextHolder.clearContext();
  }
 }

}

The Authenticate annotation allows to set the username and a list of roles that must be used when executing a test method, the AuthenticationListener take care to configure the authentication token before test execution and to reset the security context after execution.

This is an example of a test method that uses the Authenticate annotation:

@ContextConfiguration("classpath:META-INF/spring/applicationContext*.xml")
@Listeners(AuthenticationListener.class)
public class MyServiceTest extends AbstractTestNGSpringContextTests
{

 @Autowired
 private MyService myService;

 @Test
 @Authenticate(username = "pippo", roles = {"ADMIN" })
 public void testCallHelloAsAdmin()
 {
  myService.hello();
 }

 @Test
 @Authenticate(username = "pippo", roles = {"USER" })
 public void testCallHelloAsUser()
 {
  myService.hello();
 }

 @Test
 @Authenticate(username = "pippo", roles = {"ADMIN" })
 public void testCallByeAsAdmin()
 {
  myService.bye();
 }

 @Test(expectedExceptions = AccessDeniedException.class)
 @Authenticate(username = "pippo", roles = {"USER" })
 public void testCallByeAsUser()
 {
  myService.bye();
 }
}


See also:
The demo project on GitHub
Spring Security
TestNG Listeners
Annotations

1 commento:

  1. Hi Sinossi,

    Thank you very much for this wonderful post which has saved me. I have been struggling cor three days and seen your post and code in github which practically resolved my problem.

    Thanks a lot once again.

    Best Regards
    Antony Raj Savarimuthu

    RispondiElimina