Add AuthorizationManagerFactory in Kotlin DSL

Closes gh-17860
This commit is contained in:
Rob Winch 2025-09-19 16:37:41 -05:00
parent 765bdf1ed0
commit 229c7bca5b
No known key found for this signature in database
3 changed files with 220 additions and 50 deletions

View File

@ -39,7 +39,7 @@ abstract class AbstractRequestMatcherDsl {
override val rule: String) : AuthorizationRule(rule)
protected data class MatcherAuthorizationManagerRule(val matcher: RequestMatcher,
override val rule: AuthorizationManager<RequestAuthorizationContext>) : AuthorizationManagerRule(rule)
override val rule: AuthorizationManager<in RequestAuthorizationContext>) : 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<RequestAuthorizationContext>) : AuthorizationManagerRule(rule)
override val rule: AuthorizationManager<in RequestAuthorizationContext>) : AuthorizationManagerRule(rule)
protected abstract class AuthorizationRule(open val rule: String)
protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager<RequestAuthorizationContext>)
protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager<in RequestAuthorizationContext>)
protected enum class PatternType {
ANT, MVC, PATH;

View File

@ -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<AuthorizationManagerRule>()
private val rolePrefix: String
private val roleHierarchy: RoleHierarchy
private val authorizationManagerFactory: AuthorizationManagerFactory<in RequestAuthorizationContext>
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<RequestAuthorizationContext>) {
access: AuthorizationManager<in RequestAuthorizationContext>) {
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<RequestAuthorizationContext>) {
access: AuthorizationManager<in RequestAuthorizationContext>) {
authorizationRules.add(
PatternAuthorizationManagerRule(
pattern = pattern,
@ -105,7 +103,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl {
*/
fun authorize(method: HttpMethod,
pattern: String,
access: AuthorizationManager<RequestAuthorizationContext>) {
access: AuthorizationManager<in RequestAuthorizationContext>) {
authorizationRules.add(
PatternAuthorizationManagerRule(
pattern = pattern,
@ -135,7 +133,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl {
*/
fun authorize(pattern: String,
servletPath: String,
access: AuthorizationManager<RequestAuthorizationContext>) {
access: AuthorizationManager<in RequestAuthorizationContext>) {
authorizationRules.add(
PatternAuthorizationManagerRule(
pattern = pattern,
@ -167,7 +165,7 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl {
fun authorize(method: HttpMethod,
pattern: String,
servletPath: String,
access: AuthorizationManager<RequestAuthorizationContext>) {
access: AuthorizationManager<in RequestAuthorizationContext>) {
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<RequestAuthorizationContext> {
val manager = AuthorityAuthorizationManager.hasAuthority<RequestAuthorizationContext>(authority)
return withRoleHierarchy(manager)
}
fun hasAuthority(authority: String): AuthorizationManager<in RequestAuthorizationContext> = 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<RequestAuthorizationContext> {
val manager = AuthorityAuthorizationManager.hasAnyAuthority<RequestAuthorizationContext>(*authorities)
return withRoleHierarchy(manager)
}
fun hasAnyAuthority(vararg authorities: String): AuthorizationManager<in RequestAuthorizationContext> = 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<in RequestAuthorizationContext> = 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<RequestAuthorizationContext> {
val manager = AuthorityAuthorizationManager.hasAnyRole<RequestAuthorizationContext>(this.rolePrefix, arrayOf(role))
return withRoleHierarchy(manager)
}
fun hasRole(role: String): AuthorizationManager<in RequestAuthorizationContext> = 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<RequestAuthorizationContext> {
val manager = AuthorityAuthorizationManager.hasAnyRole<RequestAuthorizationContext>(this.rolePrefix, arrayOf(*roles))
return withRoleHierarchy(manager)
}
fun hasAnyRole(vararg roles: String): AuthorizationManager<in RequestAuthorizationContext> = 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<in RequestAuthorizationContext> = 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<RequestAuthorizationContext> =
AuthorizationManager { _: Supplier<out Authentication>, _: RequestAuthorizationContext -> AuthorizationDecision(true) }
val permitAll: AuthorizationManager<in RequestAuthorizationContext>
/**
* Specify that URLs are not allowed by anyone.
*/
val denyAll: AuthorizationManager<RequestAuthorizationContext> =
AuthorizationManager { _: Supplier<out Authentication>, _: RequestAuthorizationContext -> AuthorizationDecision(false) }
val denyAll: AuthorizationManager<in RequestAuthorizationContext>
/**
* Specify that URLs are allowed by any authenticated user.
*/
val authenticated: AuthorizationManager<RequestAuthorizationContext> =
AuthenticatedAuthorizationManager.authenticated()
val authenticated: AuthorizationManager<in RequestAuthorizationContext>
/**
* Specify that URLs are allowed by users who have authenticated and were not "remembered".
* @since 6.5
*/
val fullyAuthenticated: AuthorizationManager<RequestAuthorizationContext> =
AuthenticatedAuthorizationManager.fullyAuthenticated()
val fullyAuthenticated: AuthorizationManager<in RequestAuthorizationContext>
internal fun get(): (AuthorizeHttpRequestsConfigurer<HttpSecurity>.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<in RequestAuthorizationContext> {
val specific = context.getBeanProvider<AuthorizationManagerFactory<RequestAuthorizationContext>>().getIfUnique()
if (specific != null) {
return specific
}
val type = ResolvableType.forClassWithGenerics(AuthorizationManagerFactory::class.java, Object::class.java)
val general: AuthorizationManagerFactory<in RequestAuthorizationContext>? = context.getBeanProvider<AuthorizationManagerFactory<in RequestAuthorizationContext>>(type).getIfUnique()
if (general != null) {
return general
}
val defaultFactory: DefaultAuthorizationManagerFactory<RequestAuthorizationContext> = 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<RequestAuthorizationContext>): AuthorityAuthorizationManager<RequestAuthorizationContext> {
manager.setRoleHierarchy(this.roleHierarchy)
return manager
}
}

View File

@ -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<AuthorizationManagerFactory<RequestAuthorizationContext>>()
val authzManager = this.spring.context.getBean<AuthorizationManagerFactoryRequestAuthorizationContextConfig>().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<RequestAuthorizationContext> = 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<RequestAuthorizationContext> {
val factory: AuthorizationManagerFactory<RequestAuthorizationContext> = 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<AuthorizationManagerFactory<Object>>()
val authzManager = this.spring.context.getBean<AuthorizationManagerFactoryObjectConfig>().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<Object> = 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<Object> {
val factory: AuthorizationManagerFactory<Object> = 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"
}
}
}
}