diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt index 7869f6c6c5..64249d7c80 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt @@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.web import org.springframework.context.ApplicationContext import org.springframework.http.HttpMethod +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy +import org.springframework.security.access.hierarchicalroles.RoleHierarchy import org.springframework.security.authorization.AuthenticatedAuthorizationManager import org.springframework.security.authorization.AuthorityAuthorizationManager import org.springframework.security.authorization.AuthorizationDecision @@ -65,6 +67,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { private val authorizationRules = mutableListOf() private val rolePrefix: String + private val roleHierarchy: RoleHierarchy private val HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector" private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" @@ -210,7 +213,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @return the [AuthorizationManager] with the provided authority */ fun hasAuthority(authority: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAuthority(authority) + val manager = AuthorityAuthorizationManager.hasAuthority(authority) + return withRoleHierarchy(manager) } /** @@ -220,7 +224,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @return the [AuthorizationManager] with the provided authorities */ fun hasAnyAuthority(vararg authorities: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAnyAuthority(*authorities) + val manager = AuthorityAuthorizationManager.hasAnyAuthority(*authorities) + return withRoleHierarchy(manager) } /** @@ -230,7 +235,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @return the [AuthorizationManager] with the provided role */ fun hasRole(role: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(role)) + val manager = AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(role)) + return withRoleHierarchy(manager) } /** @@ -240,7 +246,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @return the [AuthorizationManager] with the provided roles */ fun hasAnyRole(vararg roles: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(*roles)) + val manager = AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(*roles)) + return withRoleHierarchy(manager) } /** @@ -296,15 +303,34 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { constructor() { this.rolePrefix = "ROLE_" + this.roleHierarchy = NullRoleHierarchy() } constructor(context: ApplicationContext) { + val rolePrefix = resolveRolePrefix(context) + this.rolePrefix = rolePrefix + val roleHierarchy = resolveRoleHierarchy(context) + this.roleHierarchy = roleHierarchy + } + + private fun resolveRolePrefix(context: ApplicationContext): String { val beanNames = context.getBeanNamesForType(GrantedAuthorityDefaults::class.java) - if (beanNames.size > 0) { - val grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults::class.java); - this.rolePrefix = grantedAuthorityDefaults.rolePrefix - } else { - this.rolePrefix = "ROLE_" + if (beanNames.isNotEmpty()) { + return context.getBean(GrantedAuthorityDefaults::class.java).rolePrefix } + return "ROLE_"; + } + + private fun resolveRoleHierarchy(context: ApplicationContext): RoleHierarchy { + val beanNames = context.getBeanNamesForType(RoleHierarchy::class.java) + if (beanNames.isNotEmpty()) { + return context.getBean(RoleHierarchy::class.java) + } + return NullRoleHierarchy() + } + + private fun withRoleHierarchy(manager: AuthorityAuthorizationManager): AuthorityAuthorizationManager { + manager.setRoleHierarchy(this.roleHierarchy) + return manager } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt index d1544b67cd..d2ec0a76d4 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod +import org.springframework.security.access.hierarchicalroles.RoleHierarchy +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl import org.springframework.security.authorization.AuthorizationDecision import org.springframework.security.authorization.AuthorizationManager import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -892,4 +894,70 @@ class AuthorizeHttpRequestsDslTests { return GrantedAuthorityDefaults("CUSTOM_") } } + + @Test + fun `hasRole when role hierarchy configured then honor hierarchy`() { + this.spring.register(RoleHierarchyConfig::class.java).autowire() + this.mockMvc.get("/protected") { + with(httpBasic("admin", "password")) + }.andExpect { + status { + isOk() + } + } + this.mockMvc.get("/protected") { + with(httpBasic("user", "password")) + }.andExpect { + status { + isOk() + } + } + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class RoleHierarchyConfig { + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/protected", hasRole("USER")) + } + httpBasic { } + } + return http.build() + } + + @Bean + open fun roleHierarchy(): RoleHierarchy { + return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_USER") + } + + @Bean + open fun userDetailsService(): UserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + val admin = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build() + return InMemoryUserDetailsManager(user, admin) + } + + @RestController + internal class PathController { + + @RequestMapping("/protected") + fun path() { + } + + } + + } } diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 50a475bee2..1453c954ec 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -5,3 +5,5 @@ Spring Security 6.4 provides a number of new features. Below are the highlights of the release, or you can view https://github.com/spring-projects/spring-security/releases[the release notes] for a detailed listing of each feature and bug fix. - https://github.com/spring-projects/spring-security/issues/4186[gh-4186] - Support `RoleHierarchy` in `AclAuthorizationStrategyImpl` +- https://github.com/spring-projects/spring-security/issues/15136[gh-15136] - Support `RoleHierarchy` Bean in `authorizeHttpRequests` Kotlin DSL +