Here’s a setup that minimize the amount of RBAC related test code you need to write.

There are certain prerequisites:

  1. Tests are annotated with @SpringBootTest. @WebMvcTest will not work since we are mocking a security context.
  2. You have a url structure suitable for selection with wildcards, e.g. /ct/config/** has a set of rules, /ct/** has another etc.
  3. The access control rules are global. I.e. controller methods are not annotated with access rules
  4. 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.