From 229c7bca5b2f2b00ec148cfbbafaec82b2ab998c Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:37:41 -0500 Subject: [PATCH] Add AuthorizationManagerFactory in Kotlin DSL Closes gh-17860 --- .../web/AbstractRequestMatcherDsl.kt | 6 +- .../web/AuthorizeHttpRequestsDsl.kt | 106 ++++++------ .../web/AuthorizeHttpRequestsDslTests.kt | 158 ++++++++++++++++++ 3 files changed, 220 insertions(+), 50 deletions(-) diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AbstractRequestMatcherDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AbstractRequestMatcherDsl.kt index e11017eec6..a7a1faea52 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AbstractRequestMatcherDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AbstractRequestMatcherDsl.kt @@ -39,7 +39,7 @@ abstract class AbstractRequestMatcherDsl { override val rule: String) : AuthorizationRule(rule) protected data class MatcherAuthorizationManagerRule(val matcher: RequestMatcher, - override val rule: AuthorizationManager) : AuthorizationManagerRule(rule) + override val rule: AuthorizationManager) : AuthorizationManagerRule(rule) protected data class PatternAuthorizationRule(val pattern: String, val patternType: PatternType, @@ -51,11 +51,11 @@ abstract class AbstractRequestMatcherDsl { val patternType: PatternType, val servletPath: String? = null, val httpMethod: HttpMethod? = null, - override val rule: AuthorizationManager) : AuthorizationManagerRule(rule) + override val rule: AuthorizationManager) : AuthorizationManagerRule(rule) protected abstract class AuthorizationRule(open val rule: String) - protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager) + protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager) protected enum class PatternType { ANT, MVC, PATH; 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 d6f15edaf8..b27d519567 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 @@ -16,24 +16,23 @@ package org.springframework.security.config.annotation.web +import org.springframework.beans.factory.getBeanProvider import org.springframework.context.ApplicationContext +import org.springframework.core.ResolvableType 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 import org.springframework.security.authorization.AuthorizationManager +import org.springframework.security.authorization.AuthorizationManagerFactory +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer import org.springframework.security.config.core.GrantedAuthorityDefaults -import org.springframework.security.core.Authentication import org.springframework.security.web.access.IpAddressAuthorizationManager import org.springframework.security.web.access.intercept.RequestAuthorizationContext import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher -import java.util.function.Supplier /** * A Kotlin DSL to configure [HttpSecurity] request authorization using idiomatic Kotlin code. @@ -44,8 +43,7 @@ import java.util.function.Supplier class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { private val authorizationRules = mutableListOf() - private val rolePrefix: String - private val roleHierarchy: RoleHierarchy + private val authorizationManagerFactory: AuthorizationManagerFactory private val PATTERN_TYPE = PatternType.PATH @@ -57,7 +55,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * (i.e. created via hasAuthority("ROLE_USER")) */ fun authorize(matches: RequestMatcher = AnyRequestMatcher.INSTANCE, - access: AuthorizationManager) { + access: AuthorizationManager) { authorizationRules.add(MatcherAuthorizationManagerRule(matches, access)) } @@ -77,7 +75,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * (i.e. created via hasAuthority("ROLE_USER")) */ fun authorize(pattern: String, - access: AuthorizationManager) { + access: AuthorizationManager) { authorizationRules.add( PatternAuthorizationManagerRule( pattern = pattern, @@ -105,7 +103,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { */ fun authorize(method: HttpMethod, pattern: String, - access: AuthorizationManager) { + access: AuthorizationManager) { authorizationRules.add( PatternAuthorizationManagerRule( pattern = pattern, @@ -135,7 +133,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { */ fun authorize(pattern: String, servletPath: String, - access: AuthorizationManager) { + access: AuthorizationManager) { authorizationRules.add( PatternAuthorizationManagerRule( pattern = pattern, @@ -167,7 +165,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { fun authorize(method: HttpMethod, pattern: String, servletPath: String, - access: AuthorizationManager) { + access: AuthorizationManager) { authorizationRules.add( PatternAuthorizationManagerRule( pattern = pattern, @@ -185,10 +183,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, etc). * @return the [AuthorizationManager] with the provided authority */ - fun hasAuthority(authority: String): AuthorizationManager { - val manager = AuthorityAuthorizationManager.hasAuthority(authority) - return withRoleHierarchy(manager) - } + fun hasAuthority(authority: String): AuthorizationManager = this.authorizationManagerFactory.hasAuthority(authority) /** * Specify that URLs require any of the provided authorities. @@ -196,10 +191,16 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @param authorities the authorities to require (i.e. ROLE_USER, ROLE_ADMIN, etc). * @return the [AuthorizationManager] with the provided authorities */ - fun hasAnyAuthority(vararg authorities: String): AuthorizationManager { - val manager = AuthorityAuthorizationManager.hasAnyAuthority(*authorities) - return withRoleHierarchy(manager) - } + fun hasAnyAuthority(vararg authorities: String): AuthorizationManager = this.authorizationManagerFactory.hasAnyAuthority(*authorities) + + + /** + * Specify that URLs require any of the provided authorities. + * + * @param authorities the authorities to require (i.e. ROLE_USER, ROLE_ADMIN, etc). + * @return the [AuthorizationManager] with the provided authorities + */ + fun hasAllAuthorities(vararg authorities: String): AuthorizationManager = this.authorizationManagerFactory.hasAllAuthorities(*authorities) /** * Specify that URLs require a particular role. @@ -207,10 +208,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @param role the role to require (i.e. USER, ADMIN, etc). * @return the [AuthorizationManager] with the provided role */ - fun hasRole(role: String): AuthorizationManager { - val manager = AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(role)) - return withRoleHierarchy(manager) - } + fun hasRole(role: String): AuthorizationManager = this.authorizationManagerFactory.hasRole(role) /** * Specify that URLs require any of the provided roles. @@ -218,10 +216,15 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { * @param roles the roles to require (i.e. USER, ADMIN, etc). * @return the [AuthorizationManager] with the provided roles */ - fun hasAnyRole(vararg roles: String): AuthorizationManager { - val manager = AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(*roles)) - return withRoleHierarchy(manager) - } + fun hasAnyRole(vararg roles: String): AuthorizationManager = this.authorizationManagerFactory.hasAnyRole(*roles) + + /** + * Specify that URLs require any of the provided roles. + * + * @param roles the roles to require (i.e. USER, ADMIN, etc). + * @return the [AuthorizationManager] with the provided roles + */ + fun hasAllRoles(vararg roles: String): AuthorizationManager = this.authorizationManagerFactory.hasAllRoles(*roles) /** * Require a specific IP or range of IP addresses. @@ -233,27 +236,23 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { /** * Specify that URLs are allowed by anyone. */ - val permitAll: AuthorizationManager = - AuthorizationManager { _: Supplier, _: RequestAuthorizationContext -> AuthorizationDecision(true) } + val permitAll: AuthorizationManager /** * Specify that URLs are not allowed by anyone. */ - val denyAll: AuthorizationManager = - AuthorizationManager { _: Supplier, _: RequestAuthorizationContext -> AuthorizationDecision(false) } + val denyAll: AuthorizationManager /** * Specify that URLs are allowed by any authenticated user. */ - val authenticated: AuthorizationManager = - AuthenticatedAuthorizationManager.authenticated() + val authenticated: AuthorizationManager /** * Specify that URLs are allowed by users who have authenticated and were not "remembered". * @since 6.5 */ - val fullyAuthenticated: AuthorizationManager = - AuthenticatedAuthorizationManager.fullyAuthenticated() + val fullyAuthenticated: AuthorizationManager internal fun get(): (AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry) -> Unit { return { requests -> @@ -274,16 +273,34 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { } } - constructor() { - this.rolePrefix = "ROLE_" - this.roleHierarchy = NullRoleHierarchy() + constructor(context: ApplicationContext) { + this.authorizationManagerFactory = resolveAuthorizationManagerFactory(context) + this.authenticated = this.authorizationManagerFactory.authenticated() + this.denyAll = this.authorizationManagerFactory.denyAll() + this.fullyAuthenticated = this.authorizationManagerFactory.fullyAuthenticated() + this.permitAll = this.authorizationManagerFactory.permitAll() } - constructor(context: ApplicationContext) { + private fun resolveAuthorizationManagerFactory(context: ApplicationContext): AuthorizationManagerFactory { + val specific = context.getBeanProvider>().getIfUnique() + if (specific != null) { + return specific + } + val type = ResolvableType.forClassWithGenerics(AuthorizationManagerFactory::class.java, Object::class.java) + val general: AuthorizationManagerFactory? = context.getBeanProvider>(type).getIfUnique() + if (general != null) { + return general + } + val defaultFactory: DefaultAuthorizationManagerFactory = DefaultAuthorizationManagerFactory() val rolePrefix = resolveRolePrefix(context) - this.rolePrefix = rolePrefix + if (rolePrefix != null) { + defaultFactory.setRolePrefix(rolePrefix) + } val roleHierarchy = resolveRoleHierarchy(context) - this.roleHierarchy = roleHierarchy + if (roleHierarchy != null) { + defaultFactory.setRoleHierarchy(roleHierarchy) + } + return defaultFactory } private fun resolveRolePrefix(context: ApplicationContext): String { @@ -301,9 +318,4 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { } 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 ed7bdd2e74..6fd4ecf88e 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 @@ -16,12 +16,16 @@ package org.springframework.security.config.annotation.web +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import jakarta.servlet.DispatcherType import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.UnsatisfiedDependencyException import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.getBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod @@ -31,6 +35,7 @@ import org.springframework.security.authentication.RememberMeAuthenticationToken import org.springframework.security.authentication.TestAuthentication import org.springframework.security.authorization.AuthorizationDecision import org.springframework.security.authorization.AuthorizationManager +import org.springframework.security.authorization.AuthorizationManagerFactory import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.core.GrantedAuthorityDefaults @@ -962,4 +967,157 @@ class AuthorizeHttpRequestsDslTests { } } } + + @Test + fun `custom AuthorizationManagerFactory of RequestAuthorizationContext`() { + this.spring.register(AuthorizationManagerFactoryRequestAuthorizationContextConfig::class.java).autowire() + val authzManagerFactory = + this.spring.context.getBean>() + val authzManager = this.spring.context.getBean().authorizationManager + every { authzManager.authorize(any(), any()) } returns AuthorizationDecision(true) + + verify { authzManagerFactory.authenticated() } + verify { authzManagerFactory.denyAll() } + verify { authzManagerFactory.fullyAuthenticated() } + verify { authzManagerFactory.hasAllAuthorities("USER", "ADMIN") } + verify { authzManagerFactory.hasAllRoles("USER", "ADMIN") } + verify { authzManagerFactory.hasAnyAuthority("USER", "ADMIN") } + verify { authzManagerFactory.hasAnyRole("USER", "ADMIN") } + verify { authzManagerFactory.hasAuthority("USER") } + verify { authzManagerFactory.hasRole("USER") } + verify { authzManagerFactory.permitAll() } + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class AuthorizationManagerFactoryRequestAuthorizationContextConfig { + val authorizationManager: AuthorizationManager = mockk() + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/authenticated", authenticated) + authorize("/denyAll", denyAll) + authorize("/fullyAuthenticated", fullyAuthenticated) + authorize("/hasAllAuthorities/user_admin", hasAllAuthorities("USER", "ADMIN")) + authorize("/hasAllRoles/user_admin", hasAllRoles("USER", "ADMIN")) + authorize("/hasAnyAuthority/user_admin", hasAnyAuthority("USER", "ADMIN")) + authorize("/hasAnyRole/user_admin", hasAnyRole("USER", "ADMIN")) + authorize("/hasAuthority/user", hasAuthority("USER")) + authorize("/hasRole/user", hasRole("USER")) + authorize("/permitAll", authenticated) + } + httpBasic { } + rememberMe { } + } + return http.build() + } + + @Bean + open fun authorizationManagerFactory(): AuthorizationManagerFactory { + val factory: AuthorizationManagerFactory = mockk() + every { factory.authenticated() } returns this.authorizationManager + every { factory.denyAll() } returns this.authorizationManager + every { factory.fullyAuthenticated() } returns this.authorizationManager + every { factory.hasAllAuthorities("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAllRoles("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAnyAuthority("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAnyRole("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAuthority(any()) } returns this.authorizationManager + every { factory.hasRole(any()) } returns this.authorizationManager + every { factory.permitAll() } returns this.authorizationManager + + return factory + } + + @Bean + open fun userDetailsService(): UserDetailsService = InMemoryUserDetailsManager(TestAuthentication.user()) + + @RestController + internal class OkController { + @GetMapping("/**") + fun ok(): String { + return "ok" + } + } + + } + + @Test + fun `custom AuthorizationManagerFactory of Object`() { + this.spring.register(AuthorizationManagerFactoryObjectConfig::class.java).autowire() + val authzManagerFactory = + this.spring.context.getBean>() + val authzManager = this.spring.context.getBean().authorizationManager + every { authzManager.authorize(any(), any()) } returns AuthorizationDecision(true) + + verify { authzManagerFactory.authenticated() } + verify { authzManagerFactory.denyAll() } + verify { authzManagerFactory.fullyAuthenticated() } + verify { authzManagerFactory.hasAllAuthorities("USER", "ADMIN") } + verify { authzManagerFactory.hasAllRoles("USER", "ADMIN") } + verify { authzManagerFactory.hasAnyAuthority("USER", "ADMIN") } + verify { authzManagerFactory.hasAnyRole("USER", "ADMIN") } + verify { authzManagerFactory.hasAuthority("USER") } + verify { authzManagerFactory.hasRole("USER") } + verify { authzManagerFactory.permitAll() } + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class AuthorizationManagerFactoryObjectConfig { + val authorizationManager: AuthorizationManager = mockk() + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/authenticated", authenticated) + authorize("/denyAll", denyAll) + authorize("/fullyAuthenticated", fullyAuthenticated) + authorize("/hasAllAuthorities/user_admin", hasAllAuthorities("USER", "ADMIN")) + authorize("/hasAllRoles/user_admin", hasAllRoles("USER", "ADMIN")) + authorize("/hasAnyAuthority/user_admin", hasAnyAuthority("USER", "ADMIN")) + authorize("/hasAnyRole/user_admin", hasAnyRole("USER", "ADMIN")) + authorize("/hasAuthority/user", hasAuthority("USER")) + authorize("/hasRole/user", hasRole("USER")) + authorize("/permitAll", authenticated) + } + httpBasic { } + rememberMe { } + } + return http.build() + } + + @Bean + open fun authorizationManagerFactory(): AuthorizationManagerFactory { + val factory: AuthorizationManagerFactory = mockk() + every { factory.authenticated() } returns this.authorizationManager + every { factory.denyAll() } returns this.authorizationManager + every { factory.fullyAuthenticated() } returns this.authorizationManager + every { factory.hasAllAuthorities("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAllRoles("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAnyAuthority("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAnyRole("USER", "ADMIN") } returns this.authorizationManager + every { factory.hasAuthority(any()) } returns this.authorizationManager + every { factory.hasRole(any()) } returns this.authorizationManager + every { factory.permitAll() } returns this.authorizationManager + + return factory + } + + @Bean + open fun userDetailsService(): UserDetailsService = InMemoryUserDetailsManager(TestAuthentication.user()) + + @RestController + internal class OkController { + @GetMapping("/**") + fun ok(): String { + return "ok" + } + } + } }