3

In our Spring Boot project we secured each method with @PreAuthorize annotation. It checks if user has permission for the requested resource.

Here is one of our controllers:

@PreAuthorize("@SecurityPermission.hasPermission('role')")
@RequestMapping(value = "/role")
public class RoleController {
    @Autowired
    private RoleService roleService;

    @PreAuthorize("@SecurityPermission.hasPermission('role.list')")
    @RequestMapping(value = "/allroles", method = RequestMethod.GET, consumes = "application/json", produces = "application/json")
    public JsonData<Role> getListOfRoles() {
        JsonData<Role> roleJsonData = new JsonData<>();
        roleJsonData.setData(roleService.list());
        return roleJsonData;
    }

}    

The question is: How to test properly permissions for above mentioned method?

I have tried the following two options:

@RunWith(SpringRunner.class)
@WebMvcTest(RoleController.class)
public class RoleControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private RoleService roleService;


    @Test
    public void optionOne() throws Exception {
        ArrayList<Role> roles = new ArrayList<>();
        roles.add(new Role().setId(1L).setName("administrator"));
        roles.add(new Role().setId(2L).setName("user"));
        given(roleService.list()).willReturn(roles);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", "application/json");

        this.mvc.perform(get("/role/allroles").with(user("testadmin"))
                .headers(headers))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data[0].name", is( roles.get(0).getName())))
                .andExpect(jsonPath("$.data[1].name", is( roles.get(1).getName())));
    }


    @Test
    @WithMockUser(authorities = {"role.list"})
    public void optionTwo() throws Exception {
        ArrayList<Role> roles = new ArrayList<>();
        roles.add(new Role().setId(1L).setName("administrator"));
        roles.add(new Role().setId(2L).setName("user"));
        given(roleService.list()).willReturn(roles);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", "application/json");

        this.mvc.perform(get("/role/allroles")
                .headers(headers))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data[0].name", is( roles.get(0).getName())))
                .andExpect(jsonPath("$.data[1].name", is( roles.get(1).getName())));
    }

}

optionOne passes even though mock user doesn't have required permission("role.list") while optionTwo failes with the status 403.

java.lang.AssertionError: Status 
Expected :200
Actual   :403

UPDATE: I am adding WebSecurityConfig class

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
    public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/auth/login";
    public static final String SEARCH_BASED_ENTRY_POINT = "/search/**";
    public static final String TOKEN_REFRESH_ENTRY_POINT = "/auth/token";
    public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/**";

    @Autowired
    private RestAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AuthenticationSuccessHandler successHandler;
    @Autowired
    private AuthenticationFailureHandler failureHandler;
    @Autowired
    private AjaxAuthenticationProvider ajaxAuthenticationProvider;
    @Autowired
    private JwtAuthenticationProvider jwtAuthenticationProvider;
    @Autowired
    private TokenExtractor tokenExtractor;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    @Bean
    protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
        List<String> pathsToSkip = Arrays.asList(TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, SEARCH_BASED_ENTRY_POINT);
        SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
        JwtTokenAuthenticationProcessingFilter filter
                = new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(ajaxAuthenticationProvider);
        auth.authenticationProvider(jwtAuthenticationProvider);

    }

    @Bean
    protected Md5PasswordEncoder passwordEncoder() {
        return new Md5PasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding("utf-8");
        filter.setForceEncoding(true);
        http.addFilterBefore(filter, CsrfFilter.class);

        http.addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class);
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(this.authenticationEntryPoint)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll()
                .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll()
                .antMatchers(SEARCH_BASED_ENTRY_POINT).permitAll()
                .and()
                .authorizeRequests()
                .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()
                .and()
                .addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }

}
valijon
  • 1,304
  • 2
  • 20
  • 35
  • It would be enough if you'd unit test the method SecurityPermission.hasPermission('role') because the annotation has been tested in the corresponding library and should work. Do you really want an integration test for that? – getjackx Mar 27 '18 at 10:35
  • 1
    If someone temporary comments the annotation and forgets about it, tests should fail letting us know that code is broken – valijon Mar 27 '18 at 10:50
  • Fair enough though if something like this happens you should fix your QA process. If the answer below does not fix the problem, could you maybe show us the code for the bean SecurityPermission? Because this is were the check happens. – getjackx Mar 27 '18 at 11:05
  • Role extracted from JWT payload and each role has set of permissions – valijon Mar 27 '18 at 11:23
  • Method security is based on permissions – valijon Mar 27 '18 at 11:25
  • Oh just noticed that optionTwo() calls `this.mvc.perform(get("/role/allroles")` without `with(user('someone'))` . Does this explain the behavior? optionOne() gets the admin user but optionTwo() has an anonymous user. – getjackx Mar 27 '18 at 11:35
  • optionTwo() uses @WithMockUser(authorities = {"role.list"}) – valijon Mar 27 '18 at 12:00

1 Answers1

1

I had the same problem a few months ago but in a slightly different way. I think your context is not setup correctly, since you have to apply SpringSecurity explicitiy to it for testing purposes:

private MockMvc mockMvc;

@Autowired
private WebApplicationContext context;

@Before
public void setup() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
            .apply(springSecurity())
            .build();
}

You can also refer to: How to unit test a secured controller which uses thymeleaf (without getting TemplateProcessingException)? It is slightly different to your problem, but since SecurityHandling is kind of an individual setup, it ist hard to help without knowing your project better.

If you are trying to test the behaviour for a non-authorized User, you can also do something like this:

@Test
public void getLoginSuccessWithAnonymousUserReturnsAccessDeniedException() throws Exception {

    MvcResult mvcResult = mockMvc.perform(get("/your-url").with(anonymous()))
            .andExpect(status().is3xxRedirection()) //change to your code
            .andReturn();

    Class result = mvcResult.getResolvedException().getClass();
    MatcherAssert.assertThat((result.equals(org.springframework.security.access.AccessDeniedException.class)), is(true));
}