Description
Describe the bug
Declaring RoleHierarchy
in a @Bean
behave differently when declared manually to AuthorityAuthorizationManager
.
I'm using spring-boot-3.1.4
To Reproduce
Please refer to my repo for full example.
I have the following security configuration
@EnableMethodSecurity
@EnableWebSecurity(debug = false)
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(httpReq -> httpReq.anyRequest().access(haveReadPermission()))
.build();
}
// see below for the configuration for RoleHierarchy and AuthorityAuthorizationManager
}
And the following test
@WebMvcTest(MeController.class)
@Import({ SecurityConfig.class})
class MeControllerTests {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = { "ADMIN" }, authorities = { "ROLE_ADMIN" } )
void test1() throws Exception {
this.mockMvc
.perform(MockMvcRequestBuilders
.get("/me"))
.andDo(MockMvcResultHandlers.print());
}
}
Pass Scenario
Given the following configuration; manually passing in role hierarchy, the test will pass
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String roleHierarchyFromMap = """
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_STAFF > ROLE_GUEST
""";
roleHierarchy.setHierarchy(roleHierarchyFromMap);
return roleHierarchy;
}
private AuthorizationManager<RequestAuthorizationContext> haveReadPermission() {
AuthorityAuthorizationManager<RequestAuthorizationContext> authority = AuthorityAuthorizationManager.hasAnyAuthority("ROLE_USER");
authority.setRoleHierarchy(this.roleHierarchy());
return authority;
}
click to view test result
MockHttpServletRequest:
HTTP Method = GET
Request URI = /me
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = com.bwgjoseph.springsecurityrolehierarchybug.MeController
Method = com.bwgjoseph.springsecurityrolehierarchybug.MeController#me(MyUserDetails)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = application/json
Body = {"username":"rob","email":"[email protected]","roles":["ROLE_ADMIN"],"authorities":[{"authority":"ROLE_ADMIN"}],"enabled":true,"password":"N/A","credentialsNonExpired":true,"accountNonExpired":true,"accountNonLocked":true}
Forwarded URL = null
Redirected URL = null
Cookies = []
Fail Scenario
With the following configuration; configure RoleHierarchy
as @Bean
and remove manually passing in role hierarchy. The test will fail.
@Bean // enable this
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String roleHierarchyFromMap = """
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_STAFF > ROLE_GUEST
""";
roleHierarchy.setHierarchy(roleHierarchyFromMap);
return roleHierarchy;
}
private AuthorizationManager<RequestAuthorizationContext> haveReadPermission() {
AuthorityAuthorizationManager<RequestAuthorizationContext> authority = AuthorityAuthorizationManager.hasAnyAuthority("ROLE_USER");
// authority.setRoleHierarchy(this.roleHierarchy()); // remove this
return authority;
}
click to view test result
MockHttpServletRequest:
HTTP Method = GET
Request URI = /me
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Expected behavior
I expect that AuthorityAuthorizationManager
should have the same behavior whether RoleHierarchy
is defined as a @Bean
or manually through the setter
.
This issue - #13188 - seem to suggest that this feature should be in working state after spring-boot 3.1.0
Possible related issue
- AuthorityAuthorizationManager never using defined role hierarchy #12473
- Role Hierarchy in authorizeHttpRequests() of HttpSecurity #13188
- Fix RoleHirerarchy usage in Example 17 - Authorization module vrudas/spring-framework-examples#101
Sample
Click the link to a GitHub repository with a minimal, reproducible sample.