Efficient role based access control testing in Spring
Here’s a setup that minimize the amount of RBAC related test code you need to write.
There are certain prerequisites:
- Tests are annotated with @SpringBootTest. @WebMvcTest will not work since we are mocking a security context.
- You have a url structure suitable for selection with wildcards, e.g.
/ct/config/**
has a set of rules,/ct/**
has another etc. - The access control rules are global. I.e. controller methods are not annotated with access rules
- Access control tests may or may not be separated from the functional tests. In our case we had a lot of functional tests before we started to the RBAC tests, so instead of rewriting them, the RBAC tests was put into separate test classes. But thanks to pt 2 above, we only needed two such classes.
Global access rules
This is typically done in either a WebSecurityConfigurerAdapter
(Classic web apps) or ResourceServerConfigurerAdapter
(OAuth2 resource servers). These classes has a method called
void configure(HttpSecurity http)
which allows you to configure the HttpSecurity
object. Example:
@Override
public void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions()
.sameOrigin() /* Needed for ADAL token refresh */
.and()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/ct/config/**").access("hasRole('SUPER_USER')")
.antMatchers(HttpMethod.POST, "/ct/config/**").access("hasRole('SUPER_USER')")
.antMatchers(HttpMethod.PUT, "/ct/config/**").access("hasRole('SUPER_USER')")
.antMatchers(HttpMethod.PATCH, "/ct/config/**").access("hasRole('SUPER_USER')")
.antMatchers(HttpMethod.GET, "/ct/config/**").access("hasRole('READ_ONLY_USER') or hasRole('USER') or hasRole('SUPER_USER')")
.antMatchers(HttpMethod.DELETE, "/ct/**").access("hasRole('USER') or hasRole('SUPER_USER')")
.antMatchers(HttpMethod.POST, "/ct/**").access("hasRole('USER') or hasRole('SUPER_USER')")
.antMatchers(HttpMethod.PUT, "/ct/**").access("hasRole('USER') or hasRole('SUPER_USER')")
.antMatchers(HttpMethod.PATCH, "/ct/**").access("hasRole('USER') or hasRole('SUPER_USER')")
.antMatchers(HttpMethod.GET, "/ct/**").access("hasRole('READ_ONLY_USER') or hasRole('USER') or hasRole('SUPER_USER')")
.antMatchers("/**").permitAll();
}
These rules are evaulated in order, so if the request url matches POST /ct/config/**
, processing will stop there.
As mentioned, the alternative is to annotate each and every controller method, which I believe is a lot more work (both maintaining and testing).
Access control tests
Since we are mocking a security context, we can predefine a set of test users with different roles, such as this:
@Configuration
public class SecurityTestConfiguration {
@Autowired
private WebApplicationContext webApplicationContext;
@Bean
@Primary
public UserDetailsService userDetailsService() {
UserDetails basicUser = User.builder().
username("user@equinor.com").
authorities("ROLE_USER").
password("password").build();
UserDetails readOnlyUser = User.builder().
username("readonlyuser@equinor.com").
authorities("ROLE_READ_ONLY_USER").
password("password").build();
UserDetails superUser = User.builder().
username("superuser@equinor.com").
authorities("ROLE_SUPER_USER").
password("password").build();
...
The tests themselves then look like this:
@Test
@WithUserDetails("readonlyuser@equinor.com")
public void test_readOnlyUserAccess() throws Exception {
...
mockMvc.perform(MockMvcRequestBuilders
.post("/ct/cargo")
.content(objectMapper.writeValueAsString(cargoResource))
.contentType(MediaType.APPLICATION_JSON_UTF8)
).andExpect(status().isForbidden());
...
}
Due to point 2 above (wildcards), we do not need to test additional endpoints such as /ct/cargo/details. Of course we are trusting that springs wildcard expansion is working, but that should be covered by tests on their end.