Browse Source

Add authorizeHttpRequests to Kotlin DSL

Closes gh-10481
Yuriy Savchenko 3 years ago
parent
commit
446ab5047c

+ 14 - 1
config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -17,6 +17,8 @@
 package org.springframework.security.config.web.servlet
 
 import org.springframework.http.HttpMethod
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
 import org.springframework.security.web.util.matcher.AnyRequestMatcher
 import org.springframework.security.web.util.matcher.RequestMatcher
 
@@ -36,14 +38,25 @@ abstract class AbstractRequestMatcherDsl {
     protected data class MatcherAuthorizationRule(val matcher: RequestMatcher,
                                                   override val rule: String) : AuthorizationRule(rule)
 
+    protected data class MatcherAuthorizationManagerRule(val matcher: RequestMatcher,
+                                                         override val rule: AuthorizationManager<RequestAuthorizationContext>) : AuthorizationManagerRule(rule)
+
     protected data class PatternAuthorizationRule(val pattern: String,
                                                   val patternType: PatternType,
                                                   val servletPath: String? = null,
                                                   val httpMethod: HttpMethod? = null,
                                                   override val rule: String) : AuthorizationRule(rule)
 
+    protected data class PatternAuthorizationManagerRule(val pattern: String,
+                                                         val patternType: PatternType,
+                                                         val servletPath: String? = null,
+                                                         val httpMethod: HttpMethod? = null,
+                                                         override val rule: AuthorizationManager<RequestAuthorizationContext>) : AuthorizationManagerRule(rule)
+
     protected abstract class AuthorizationRule(open val rule: String)
 
+    protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager<RequestAuthorizationContext>)
+
     protected enum class PatternType {
         ANT, MVC
     }

+ 253 - 0
config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt

@@ -0,0 +1,253 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.servlet
+
+import org.springframework.http.HttpMethod
+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.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
+import org.springframework.security.core.Authentication
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
+import org.springframework.security.web.util.matcher.AnyRequestMatcher
+import org.springframework.security.web.util.matcher.RequestMatcher
+import org.springframework.util.ClassUtils
+import java.util.function.Supplier
+
+/**
+ * A Kotlin DSL to configure [HttpSecurity] request authorization using idiomatic Kotlin code.
+ *
+ * @author Yuriy Savchenko
+ * @since 5.7
+ */
+class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() {
+    private val authorizationRules = mutableListOf<AuthorizationManagerRule>()
+
+    private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector"
+    private val MVC_PRESENT = ClassUtils.isPresent(
+        HANDLER_MAPPING_INTROSPECTOR,
+        AuthorizeHttpRequestsDsl::class.java.classLoader)
+    private val PATTERN_TYPE = if (MVC_PRESENT) PatternType.MVC else PatternType.ANT
+
+    /**
+     * Adds a request authorization rule.
+     *
+     * @param matches the [RequestMatcher] to match incoming requests against
+     * @param access the [AuthorizationManager] to secure the matching request
+     * (i.e. created via hasAuthority("ROLE_USER"))
+     */
+    fun authorize(matches: RequestMatcher = AnyRequestMatcher.INSTANCE,
+                  access: AuthorizationManager<RequestAuthorizationContext>) {
+        authorizationRules.add(MatcherAuthorizationManagerRule(matches, access))
+    }
+
+    /**
+     * Adds a request authorization rule for an endpoint matching the provided
+     * pattern.
+     * If Spring MVC is on the classpath, it will use an MVC matcher.
+     * If Spring MVC is not on the classpath, it will use an ant matcher.
+     * The MVC will use the same rules that Spring MVC uses for matching.
+     * For example, often times a mapping of the path "/path" will match on
+     * "/path", "/path/", "/path.html", etc.
+     * If the current request will not be processed by Spring MVC, a reasonable default
+     * using the pattern as an ant pattern will be used.
+     *
+     * @param pattern the pattern to match incoming requests against.
+     * @param access the [AuthorizationManager] to secure the matching request
+     * (i.e. created via hasAuthority("ROLE_USER"))
+     */
+    fun authorize(pattern: String,
+                  access: AuthorizationManager<RequestAuthorizationContext>) {
+        authorizationRules.add(
+            PatternAuthorizationManagerRule(
+                pattern = pattern,
+                patternType = PATTERN_TYPE,
+                rule = access
+            )
+        )
+    }
+
+    /**
+     * Adds a request authorization rule for an endpoint matching the provided
+     * pattern.
+     * If Spring MVC is on the classpath, it will use an MVC matcher.
+     * If Spring MVC is not on the classpath, it will use an ant matcher.
+     * The MVC will use the same rules that Spring MVC uses for matching.
+     * For example, often times a mapping of the path "/path" will match on
+     * "/path", "/path/", "/path.html", etc.
+     * If the current request will not be processed by Spring MVC, a reasonable default
+     * using the pattern as an ant pattern will be used.
+     *
+     * @param method the HTTP method to match the income requests against.
+     * @param pattern the pattern to match incoming requests against.
+     * @param access the [AuthorizationManager] to secure the matching request
+     * (i.e. created via hasAuthority("ROLE_USER"))
+     */
+    fun authorize(method: HttpMethod,
+                  pattern: String,
+                  access: AuthorizationManager<RequestAuthorizationContext>) {
+        authorizationRules.add(
+            PatternAuthorizationManagerRule(
+                pattern = pattern,
+                patternType = PATTERN_TYPE,
+                httpMethod = method,
+                rule = access
+            )
+        )
+    }
+
+    /**
+     * Adds a request authorization rule for an endpoint matching the provided
+     * pattern.
+     * If Spring MVC is on the classpath, it will use an MVC matcher.
+     * If Spring MVC is not on the classpath, it will use an ant matcher.
+     * The MVC will use the same rules that Spring MVC uses for matching.
+     * For example, often times a mapping of the path "/path" will match on
+     * "/path", "/path/", "/path.html", etc.
+     * If the current request will not be processed by Spring MVC, a reasonable default
+     * using the pattern as an ant pattern will be used.
+     *
+     * @param pattern the pattern to match incoming requests against.
+     * @param servletPath the servlet path to match incoming requests against. This
+     * only applies when using an MVC pattern matcher.
+     * @param access the [AuthorizationManager] to secure the matching request
+     * (i.e. created via hasAuthority("ROLE_USER"))
+     */
+    fun authorize(pattern: String,
+                  servletPath: String,
+                  access: AuthorizationManager<RequestAuthorizationContext>) {
+        authorizationRules.add(
+            PatternAuthorizationManagerRule(
+                pattern = pattern,
+                patternType = PATTERN_TYPE,
+                servletPath = servletPath,
+                rule = access
+            )
+        )
+    }
+
+    /**
+     * Adds a request authorization rule for an endpoint matching the provided
+     * pattern.
+     * If Spring MVC is on the classpath, it will use an MVC matcher.
+     * If Spring MVC is not on the classpath, it will use an ant matcher.
+     * The MVC will use the same rules that Spring MVC uses for matching.
+     * For example, often times a mapping of the path "/path" will match on
+     * "/path", "/path/", "/path.html", etc.
+     * If the current request will not be processed by Spring MVC, a reasonable default
+     * using the pattern as an ant pattern will be used.
+     *
+     * @param method the HTTP method to match the income requests against.
+     * @param pattern the pattern to match incoming requests against.
+     * @param servletPath the servlet path to match incoming requests against. This
+     * only applies when using an MVC pattern matcher.
+     * @param access the [AuthorizationManager] to secure the matching request
+     * (i.e. created via hasAuthority("ROLE_USER"))
+     */
+    fun authorize(method: HttpMethod,
+                  pattern: String,
+                  servletPath: String,
+                  access: AuthorizationManager<RequestAuthorizationContext>) {
+        authorizationRules.add(
+            PatternAuthorizationManagerRule(
+                pattern = pattern,
+                patternType = PATTERN_TYPE,
+                servletPath = servletPath,
+                httpMethod = method,
+                rule = access
+            )
+        )
+    }
+
+    /**
+     * Specify that URLs require a particular authority.
+     *
+     * @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> {
+        return AuthorityAuthorizationManager.hasAuthority(authority)
+    }
+
+    /**
+     * 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 hasAnyAuthority(vararg authorities: String): AuthorizationManager<RequestAuthorizationContext> {
+        return AuthorityAuthorizationManager.hasAnyAuthority(*authorities)
+    }
+
+    /**
+     * Specify that URLs require a particular role.
+     *
+     * @param role the role to require (i.e. USER, ADMIN, etc).
+     * @return the [AuthorizationManager] with the provided role
+     */
+    fun hasRole(role: String): AuthorizationManager<RequestAuthorizationContext> {
+        return AuthorityAuthorizationManager.hasRole(role)
+    }
+
+    /**
+     * 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 hasAnyRole(vararg roles: String): AuthorizationManager<RequestAuthorizationContext> {
+        return AuthorityAuthorizationManager.hasAnyRole(*roles)
+    }
+
+    /**
+     * Specify that URLs are allowed by anyone.
+     */
+    val permitAll: AuthorizationManager<RequestAuthorizationContext> =
+        AuthorizationManager { _: Supplier<Authentication>, _: RequestAuthorizationContext -> AuthorizationDecision(true) }
+
+    /**
+     * Specify that URLs are not allowed by anyone.
+     */
+    val denyAll: AuthorizationManager<RequestAuthorizationContext> =
+        AuthorizationManager { _: Supplier<Authentication>, _: RequestAuthorizationContext -> AuthorizationDecision(false) }
+
+    /**
+     * Specify that URLs are allowed by any authenticated user.
+     */
+    val authenticated: AuthorizationManager<RequestAuthorizationContext> =
+        AuthenticatedAuthorizationManager.authenticated()
+
+    internal fun get(): (AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry) -> Unit {
+        return { requests ->
+            authorizationRules.forEach { rule ->
+                when (rule) {
+                    is MatcherAuthorizationManagerRule -> requests.requestMatchers(rule.matcher).access(rule.rule)
+                    is PatternAuthorizationManagerRule -> {
+                        when (rule.patternType) {
+                            PatternType.ANT -> requests.antMatchers(rule.httpMethod, rule.pattern).access(rule.rule)
+                            PatternType.MVC -> requests.mvcMatchers(rule.httpMethod, rule.pattern)
+                                .apply { if (rule.servletPath != null) servletPath(rule.servletPath) }
+                                .access(rule.rule)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 36 - 1
config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt

@@ -101,7 +101,10 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
     fun securityMatcher(vararg pattern: String) {
         val mvcPresent = ClassUtils.isPresent(
                 HANDLER_MAPPING_INTROSPECTOR,
-                AuthorizeRequestsDsl::class.java.classLoader)
+                AuthorizeRequestsDsl::class.java.classLoader) ||
+                ClassUtils.isPresent(
+                    HANDLER_MAPPING_INTROSPECTOR,
+                    AuthorizeHttpRequestsDsl::class.java.classLoader)
         this.http.requestMatchers {
             if (mvcPresent) {
                 it.mvcMatchers(*pattern)
@@ -198,6 +201,38 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
         this.http.authorizeRequests(authorizeRequestsCustomizer)
     }
 
+    /**
+     * Allows restricting access based upon the [HttpServletRequest]
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+     *      http {
+     *          authorizeHttpRequests {
+     *              authorize("/public", permitAll)
+     *              authorize(anyRequest, authenticated)
+     *          }
+     *      }
+     *      return http.build()
+     *  }
+     * }
+     * ```
+     *
+     * @param authorizeHttpRequestsConfiguration custom configuration that specifies
+     * access for requests
+     * @see [AuthorizeHttpRequestsDsl]
+     * @since 5.7
+     */
+    fun authorizeHttpRequests(authorizeHttpRequestsConfiguration: AuthorizeHttpRequestsDsl.() -> Unit) {
+        val authorizeHttpRequestsCustomizer = AuthorizeHttpRequestsDsl().apply(authorizeHttpRequestsConfiguration).get()
+        this.http.authorizeHttpRequests(authorizeHttpRequestsCustomizer)
+    }
+
     /**
      * Enables HTTP basic authentication.
      *

+ 644 - 0
config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDslTests.kt

@@ -0,0 +1,644 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.servlet
+
+import org.assertj.core.api.Assertions.*
+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.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpMethod
+import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.provisioning.InMemoryUserDetailsManager
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
+import org.springframework.security.web.util.matcher.RegexRequestMatcher
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.post
+import org.springframework.test.web.servlet.put
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
+import java.util.function.Supplier
+
+/**
+ * Tests for [AuthorizeHttpRequestsDsl]
+ *
+ * @author Yuriy Savchenko
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class AuthorizeHttpRequestsDslTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `request when secured by regex matcher then responds with forbidden`() {
+        this.spring.register(AuthorizeHttpRequestsByRegexConfig::class.java).autowire()
+
+        this.mockMvc.get("/private")
+            .andExpect {
+                status { isForbidden() }
+            }
+    }
+
+    @Test
+    fun `request when allowed by regex matcher then responds with ok`() {
+        this.spring.register(AuthorizeHttpRequestsByRegexConfig::class.java).autowire()
+
+        this.mockMvc.get("/path")
+            .andExpect {
+                status { isOk() }
+            }
+    }
+
+    @Test
+    fun `request when allowed by regex matcher with http method then responds based on method`() {
+        this.spring.register(AuthorizeHttpRequestsByRegexConfig::class.java).autowire()
+
+        this.mockMvc.post("/onlyPostPermitted") { with(csrf()) }
+            .andExpect {
+                status { isOk() }
+            }
+
+        this.mockMvc.get("/onlyPostPermitted")
+            .andExpect {
+                status { isForbidden() }
+            }
+    }
+
+    @EnableWebSecurity
+    open class AuthorizeHttpRequestsByRegexConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize(RegexRequestMatcher("/path", null), permitAll)
+                    authorize(RegexRequestMatcher("/onlyPostPermitted", "POST"), permitAll)
+                    authorize(RegexRequestMatcher("/onlyPostPermitted", "GET"), denyAll)
+                    authorize(RegexRequestMatcher(".*", null), authenticated)
+                }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/path")
+            fun path() {
+            }
+
+            @RequestMapping("/onlyPostPermitted")
+            fun onlyPostPermitted() {
+            }
+        }
+    }
+
+    @Test
+    fun `request when secured by mvc then responds with forbidden`() {
+        this.spring.register(AuthorizeHttpRequestsByMvcConfig::class.java).autowire()
+
+        this.mockMvc.get("/private")
+            .andExpect {
+                status { isForbidden() }
+            }
+    }
+
+    @Test
+    fun `request when allowed by mvc then responds with OK`() {
+        this.spring.register(AuthorizeHttpRequestsByMvcConfig::class.java, LegacyMvcMatchingConfig::class.java).autowire()
+
+        this.mockMvc.get("/path")
+            .andExpect {
+                status { isOk() }
+            }
+
+        this.mockMvc.get("/path.html")
+            .andExpect {
+                status { isOk() }
+            }
+
+        this.mockMvc.get("/path/")
+            .andExpect {
+                status { isOk() }
+            }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class AuthorizeHttpRequestsByMvcConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize("/path", permitAll)
+                    authorize("/**", authenticated)
+                }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/path")
+            fun path() {
+            }
+        }
+    }
+
+    @Configuration
+    open class LegacyMvcMatchingConfig : WebMvcConfigurer {
+        override fun configurePathMatch(configurer: PathMatchConfigurer) {
+            configurer.setUseSuffixPatternMatch(true)
+        }
+    }
+
+    @Test
+    fun `request when secured by mvc path variables then responds based on path variable value`() {
+        this.spring.register(MvcMatcherPathVariablesConfig::class.java).autowire()
+
+        this.mockMvc.get("/user/user")
+            .andExpect {
+                status { isOk() }
+            }
+
+        this.mockMvc.get("/user/deny")
+            .andExpect {
+                status { isForbidden() }
+            }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class MvcMatcherPathVariablesConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            val access = AuthorizationManager { _: Supplier<Authentication>, context: RequestAuthorizationContext ->
+                AuthorizationDecision(context.variables["userName"] == "user")
+            }
+            http {
+                authorizeHttpRequests {
+                    authorize("/user/{userName}", access)
+                }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/user/{user}")
+            fun path(@PathVariable user: String) {
+            }
+        }
+    }
+
+    @Test
+    fun `request when user has allowed role then responds with OK`() {
+        this.spring.register(HasRoleConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("admin", "password"))
+        }.andExpect {
+            status { isOk() }
+        }
+    }
+
+    @Test
+    fun `request when user does not have allowed role then responds with forbidden`() {
+        this.spring.register(HasRoleConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("user", "password"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class HasRoleConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize("/**", hasRole("ADMIN"))
+                }
+                httpBasic { }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @GetMapping("/")
+            fun index() {
+            }
+        }
+
+        @Bean
+        open fun userDetailsService(): UserDetailsService {
+            val userDetails = User.withDefaultPasswordEncoder()
+                .username("user")
+                .password("password")
+                .roles("USER")
+                .build()
+            val adminDetails = User.withDefaultPasswordEncoder()
+                .username("admin")
+                .password("password")
+                .roles("ADMIN")
+                .build()
+            return InMemoryUserDetailsManager(userDetails, adminDetails)
+        }
+    }
+
+    @Test
+    fun `request when user has some allowed roles then responds with OK`() {
+        this.spring.register(HasAnyRoleConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("user", "password"))
+        }.andExpect {
+            status { isOk() }
+        }
+
+        this.mockMvc.get("/") {
+            with(httpBasic("admin", "password"))
+        }.andExpect {
+            status { isOk() }
+        }
+    }
+
+    @Test
+    fun `request when user does not have any allowed roles then responds with forbidden`() {
+        this.spring.register(HasAnyRoleConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("other", "password"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class HasAnyRoleConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize("/**", hasAnyRole("ADMIN", "USER"))
+                }
+                httpBasic { }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @GetMapping("/")
+            fun index() {
+            }
+        }
+
+        @Bean
+        open fun userDetailsService(): UserDetailsService {
+            val userDetails = User.withDefaultPasswordEncoder()
+                .username("user")
+                .password("password")
+                .roles("USER")
+                .build()
+            val admin1Details = User.withDefaultPasswordEncoder()
+                .username("admin")
+                .password("password")
+                .roles("ADMIN")
+                .build()
+            val admin2Details = User.withDefaultPasswordEncoder()
+                .username("other")
+                .password("password")
+                .roles("OTHER")
+                .build()
+            return InMemoryUserDetailsManager(userDetails, admin1Details, admin2Details)
+        }
+    }
+
+    @Test
+    fun `request when user has allowed authority then responds with OK`() {
+        this.spring.register(HasAuthorityConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("admin", "password"))
+        }.andExpect {
+            status { isOk() }
+        }
+    }
+
+    @Test
+    fun `request when user does not have allowed authority then responds with forbidden`() {
+        this.spring.register(HasAuthorityConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("user", "password"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class HasAuthorityConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize("/**", hasAuthority("ROLE_ADMIN"))
+                }
+                httpBasic { }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @GetMapping("/")
+            fun index() {
+            }
+        }
+
+        @Bean
+        open fun userDetailsService(): UserDetailsService {
+            val userDetails = User.withDefaultPasswordEncoder()
+                .username("user")
+                .password("password")
+                .roles("USER")
+                .build()
+            val adminDetails = User.withDefaultPasswordEncoder()
+                .username("admin")
+                .password("password")
+                .roles("ADMIN")
+                .build()
+            return InMemoryUserDetailsManager(userDetails, adminDetails)
+        }
+    }
+
+    @Test
+    fun `request when user has some allowed authorities then responds with OK`() {
+        this.spring.register(HasAnyAuthorityConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("user", "password"))
+        }.andExpect {
+            status { isOk() }
+        }
+
+        this.mockMvc.get("/") {
+            with(httpBasic("admin", "password"))
+        }.andExpect {
+            status { isOk() }
+        }
+    }
+
+    @Test
+    fun `request when user does not have any allowed authorities then responds with forbidden`() {
+        this.spring.register(HasAnyAuthorityConfig::class.java).autowire()
+
+        this.mockMvc.get("/") {
+            with(httpBasic("other", "password"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class HasAnyAuthorityConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize("/**", hasAnyAuthority("ROLE_ADMIN", "ROLE_USER"))
+                }
+                httpBasic { }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @GetMapping("/")
+            fun index() {
+            }
+        }
+
+        @Bean
+        open fun userDetailsService(): UserDetailsService {
+            val userDetails = User.withDefaultPasswordEncoder()
+                .username("user")
+                .password("password")
+                .authorities("ROLE_USER")
+                .build()
+            val admin1Details = User.withDefaultPasswordEncoder()
+                .username("admin")
+                .password("password")
+                .authorities("ROLE_ADMIN")
+                .build()
+            val admin2Details = User.withDefaultPasswordEncoder()
+                .username("other")
+                .password("password")
+                .authorities("ROLE_OTHER")
+                .build()
+            return InMemoryUserDetailsManager(userDetails, admin1Details, admin2Details)
+        }
+    }
+
+    @Test
+    fun `request when secured by mvc with servlet path then responds based on servlet path`() {
+        this.spring.register(MvcMatcherServletPathConfig::class.java).autowire()
+
+        this.mockMvc.perform(get("/spring/path")
+            .with { request ->
+                request.servletPath = "/spring"
+                request
+            })
+            .andExpect(status().isForbidden)
+
+        this.mockMvc.perform(get("/other/path")
+            .with { request ->
+                request.servletPath = "/other"
+                request
+            })
+            .andExpect(status().isOk)
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class MvcMatcherServletPathConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize("/path", "/spring", denyAll)
+                }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/path")
+            fun path() {
+            }
+        }
+    }
+
+    @Test
+    fun `request when secured by mvc with http method then responds based on http method`() {
+        this.spring.register(AuthorizeRequestsByMvcConfigWithHttpMethod::class.java).autowire()
+
+        this.mockMvc.get("/path")
+            .andExpect {
+                status { isOk() }
+            }
+
+        this.mockMvc.put("/path") { with(csrf()) }
+            .andExpect {
+                status { isForbidden() }
+            }
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class AuthorizeRequestsByMvcConfigWithHttpMethod {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize(HttpMethod.GET, "/path", permitAll)
+                    authorize(HttpMethod.PUT, "/path", denyAll)
+                }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/path")
+            fun path() {
+            }
+        }
+    }
+
+    @Test
+    fun `request when secured by mvc with servlet path and http method then responds based on path and method`() {
+        this.spring.register(MvcMatcherServletPathHttpMethodConfig::class.java).autowire()
+
+        this.mockMvc.perform(get("/spring/path")
+            .with { request ->
+                request.apply {
+                    servletPath = "/spring"
+                }
+            })
+            .andExpect(status().isForbidden)
+
+        this.mockMvc.perform(put("/spring/path")
+            .with { request ->
+                request.apply {
+                    servletPath = "/spring"
+                    csrf()
+                }
+            })
+            .andExpect(status().isForbidden)
+
+        this.mockMvc.perform(get("/other/path")
+            .with { request ->
+                request.apply {
+                    servletPath = "/other"
+                }
+            })
+            .andExpect(status().isOk)
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class MvcMatcherServletPathHttpMethodConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeHttpRequests {
+                    authorize(HttpMethod.GET, "/path", "/spring", denyAll)
+                    authorize(HttpMethod.PUT, "/path", "/spring", denyAll)
+                }
+            }
+            return http.build()
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/path")
+            fun path() {
+            }
+        }
+    }
+
+    @Test
+    fun `request when both authorizeRequests and authorizeHttpRequests configured then exception`() {
+        assertThatThrownBy { this.spring.register(BothAuthorizeRequestsConfig::class.java).autowire() }
+            .isInstanceOf(UnsatisfiedDependencyException::class.java)
+            .hasRootCauseInstanceOf(IllegalStateException::class.java)
+            .hasMessageContaining(
+                "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."
+            )
+    }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class BothAuthorizeRequestsConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authorizeRequests {
+                    authorize(anyRequest, permitAll)
+                }
+                authorizeHttpRequests {
+                    authorize(anyRequest, denyAll)
+                }
+            }
+            return http.build()
+        }
+    }
+}

+ 116 - 29
config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -53,6 +53,9 @@ import org.springframework.test.web.servlet.post
 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
 import org.springframework.web.servlet.config.annotation.EnableWebMvc
 import javax.servlet.Filter
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.springframework.security.web.SecurityFilterChain
 
 /**
  * Tests for [HttpSecurityDsl]
@@ -128,9 +131,13 @@ class HttpSecurityDslTests {
         }
     }
 
-    @Test
-    fun `request when it does not match the security request matcher then the security rules do not apply`() {
-        this.spring.register(SecurityRequestMatcherConfig::class.java).autowire()
+    @ParameterizedTest
+    @ValueSource(classes = [
+        SecurityRequestMatcherRequestsConfig::class,
+        SecurityRequestMatcherHttpRequestsConfig::class
+    ])
+    fun `request when it does not match the security request matcher then the security rules do not apply`(config: Class<*>) {
+        this.spring.register(config).autowire()
 
         this.mockMvc.get("/")
                 .andExpect {
@@ -138,9 +145,13 @@ class HttpSecurityDslTests {
                 }
     }
 
-    @Test
-    fun `request when it matches the security request matcher then the security rules apply`() {
-        this.spring.register(SecurityRequestMatcherConfig::class.java).autowire()
+    @ParameterizedTest
+    @ValueSource(classes = [
+        SecurityRequestMatcherRequestsConfig::class,
+        SecurityRequestMatcherHttpRequestsConfig::class
+    ])
+    fun `request when it matches the security request matcher then the security rules apply`(config: Class<*>) {
+        this.spring.register(config).autowire()
 
         this.mockMvc.get("/path")
                 .andExpect {
@@ -149,7 +160,7 @@ class HttpSecurityDslTests {
     }
 
     @EnableWebSecurity
-    open class SecurityRequestMatcherConfig : WebSecurityConfigurerAdapter() {
+    open class SecurityRequestMatcherRequestsConfig : WebSecurityConfigurerAdapter() {
         override fun configure(http: HttpSecurity) {
             http {
                 securityMatcher(RegexRequestMatcher("/path", null))
@@ -160,9 +171,27 @@ class HttpSecurityDslTests {
         }
     }
 
-    @Test
-    fun `request when it does not match the security pattern matcher then the security rules do not apply`() {
-        this.spring.register(SecurityPatternMatcherConfig::class.java).autowire()
+    @EnableWebSecurity
+    open class SecurityRequestMatcherHttpRequestsConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                securityMatcher(RegexRequestMatcher("/path", null))
+                authorizeHttpRequests {
+                    authorize(anyRequest, authenticated)
+                }
+            }
+            return http.build()
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(classes = [
+        SecurityPatternMatcherRequestsConfig::class,
+        SecurityPatternMatcherHttpRequestsConfig::class
+    ])
+    fun `request when it does not match the security pattern matcher then the security rules do not apply`(config: Class<*>) {
+        this.spring.register(config).autowire()
 
         this.mockMvc.get("/")
                 .andExpect {
@@ -170,9 +199,13 @@ class HttpSecurityDslTests {
                 }
     }
 
-    @Test
-    fun `request when it matches the security pattern matcher then the security rules apply`() {
-        this.spring.register(SecurityPatternMatcherConfig::class.java).autowire()
+    @ParameterizedTest
+    @ValueSource(classes = [
+        SecurityPatternMatcherRequestsConfig::class,
+        SecurityPatternMatcherHttpRequestsConfig::class
+    ])
+    fun `request when it matches the security pattern matcher then the security rules apply`(config: Class<*>) {
+        this.spring.register(config).autowire()
 
         this.mockMvc.get("/path")
                 .andExpect {
@@ -182,7 +215,7 @@ class HttpSecurityDslTests {
 
     @EnableWebSecurity
     @EnableWebMvc
-    open class SecurityPatternMatcherConfig : WebSecurityConfigurerAdapter() {
+    open class SecurityPatternMatcherRequestsConfig : WebSecurityConfigurerAdapter() {
         override fun configure(http: HttpSecurity) {
             http {
                 securityMatcher("/path")
@@ -193,9 +226,28 @@ class HttpSecurityDslTests {
         }
     }
 
-    @Test
-    fun `security pattern matcher when used with security request matcher then both apply`() {
-        this.spring.register(MultiMatcherConfig::class.java).autowire()
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class SecurityPatternMatcherHttpRequestsConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                securityMatcher("/path")
+                authorizeHttpRequests {
+                    authorize(anyRequest, authenticated)
+                }
+            }
+            return http.build()
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(classes = [
+        MultiMatcherRequestsConfig::class,
+        MultiMatcherHttpRequestsConfig::class
+    ])
+    fun `security pattern matcher when used with security request matcher then both apply`(config: Class<*>) {
+        this.spring.register(config).autowire()
 
         this.mockMvc.get("/path1")
                 .andExpect {
@@ -215,7 +267,7 @@ class HttpSecurityDslTests {
 
     @EnableWebSecurity
     @EnableWebMvc
-    open class MultiMatcherConfig : WebSecurityConfigurerAdapter() {
+    open class MultiMatcherRequestsConfig : WebSecurityConfigurerAdapter() {
         override fun configure(http: HttpSecurity) {
             http {
                 securityMatcher("/path1")
@@ -227,28 +279,48 @@ class HttpSecurityDslTests {
         }
     }
 
-    @Test
-    fun `authentication manager when configured in DSL then used`() {
-        this.spring.register(AuthenticationManagerConfig::class.java).autowire()
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class MultiMatcherHttpRequestsConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                securityMatcher("/path1")
+                securityMatcher(RegexRequestMatcher("/path2", null))
+                authorizeHttpRequests {
+                    authorize(anyRequest, authenticated)
+                }
+            }
+            return http.build()
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(classes = [
+        AuthenticationManagerRequestsConfig::class,
+        AuthenticationManagerHttpRequestsConfig::class
+    ])
+    fun `authentication manager when configured in DSL then used`(config: Class<*>) {
+        this.spring.register(config).autowire()
         mockkObject(AuthenticationManagerConfig.AUTHENTICATION_MANAGER)
         every {
             AuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any())
         } returns TestingAuthenticationToken("user", "test", "ROLE_USER")
-        val  request = MockMvcRequestBuilders.get("/")
+        val request = MockMvcRequestBuilders.get("/")
             .with(httpBasic("user", "password"))
         this.mockMvc.perform(request)
         verify(exactly = 1) { AuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any()) }
     }
 
-    @EnableWebSecurity
-    open class AuthenticationManagerConfig : WebSecurityConfigurerAdapter() {
-        companion object {
-            val AUTHENTICATION_MANAGER: AuthenticationManager = ProviderManager(TestingAuthenticationProvider())
-        }
+    object AuthenticationManagerConfig {
+        val AUTHENTICATION_MANAGER: AuthenticationManager = ProviderManager(TestingAuthenticationProvider())
+    }
 
+    @EnableWebSecurity
+    open class AuthenticationManagerRequestsConfig : WebSecurityConfigurerAdapter() {
         override fun configure(http: HttpSecurity) {
             http {
-                authenticationManager = AUTHENTICATION_MANAGER
+                authenticationManager = AuthenticationManagerConfig.AUTHENTICATION_MANAGER
                 authorizeRequests {
                     authorize(anyRequest, authenticated)
                 }
@@ -257,6 +329,21 @@ class HttpSecurityDslTests {
         }
     }
 
+    @EnableWebSecurity
+    open class AuthenticationManagerHttpRequestsConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                authenticationManager = AuthenticationManagerConfig.AUTHENTICATION_MANAGER
+                authorizeHttpRequests {
+                    authorize(anyRequest, authenticated)
+                }
+                httpBasic { }
+            }
+            return http.build()
+        }
+    }
+
     @Test
     fun `HTTP security when custom filter configured then custom filter added to filter chain`() {
         this.spring.register(CustomFilterConfig::class.java).autowire()