ソースを参照

Add authorize() DSL method that accepts HttpMethod

Fixes: gh-8307
Adam Millerchip 5 年 前
コミット
0f29bee1b0

+ 2 - 0
config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt

@@ -16,6 +16,7 @@
 
 package org.springframework.security.config.web.servlet
 
+import org.springframework.http.HttpMethod
 import org.springframework.security.web.util.matcher.AnyRequestMatcher
 import org.springframework.security.web.util.matcher.RequestMatcher
 
@@ -38,6 +39,7 @@ abstract class AbstractRequestMatcherDsl {
     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 abstract class AuthorizationRule(open val rule: String)

+ 54 - 6
config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt

@@ -16,6 +16,7 @@
 
 package org.springframework.security.config.web.servlet
 
+import org.springframework.http.HttpMethod
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
 import org.springframework.security.web.util.matcher.AnyRequestMatcher
@@ -70,6 +71,29 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() {
                                                         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 an 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 SpEL expression to secure the matching request
+     * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')")
+     */
+    fun authorize(method: HttpMethod, pattern: String, access: String = "authenticated") {
+        authorizationRules.add(PatternAuthorizationRule(pattern = pattern,
+                                                        patternType = PATTERN_TYPE,
+                                                        httpMethod = method,
+                                                        rule = access))
+    }
+
     /**
      * Adds a request authorization rule for an endpoint matching the provided
      * pattern.
@@ -94,6 +118,32 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() {
                                                         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 an 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 SpEL expression to secure the matching request
+     * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')")
+     */
+    fun authorize(method: HttpMethod, pattern: String, servletPath: String, access: String = "authenticated") {
+        authorizationRules.add(PatternAuthorizationRule(pattern = pattern,
+                                                        patternType = PATTERN_TYPE,
+                                                        servletPath = servletPath,
+                                                        httpMethod = method,
+                                                        rule = access))
+    }
+
     /**
      * Specify that URLs require a particular authority.
      *
@@ -150,12 +200,10 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() {
                     is MatcherAuthorizationRule -> requests.requestMatchers(rule.matcher).access(rule.rule)
                     is PatternAuthorizationRule -> {
                         when (rule.patternType) {
-                            PatternType.ANT -> requests.antMatchers(rule.pattern).access(rule.rule)
-                            PatternType.MVC -> {
-                                val mvcMatchersAuthorizeUrl = requests.mvcMatchers(rule.pattern)
-                                rule.servletPath?.also { mvcMatchersAuthorizeUrl.servletPath(rule.servletPath) }
-                                mvcMatchersAuthorizeUrl.access(rule.rule)
-                            }
+                            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)
                         }
                     }
                 }

+ 112 - 0
config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt

@@ -20,6 +20,7 @@ import org.junit.Rule
 import org.junit.Test
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpMethod
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
@@ -27,10 +28,13 @@ import org.springframework.security.config.test.SpringTestRule
 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.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
 import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
 import org.springframework.web.bind.annotation.GetMapping
@@ -72,12 +76,29 @@ class AuthorizeRequestsDslTests {
                 }
     }
 
+    @Test
+    fun `request when allowed by regex matcher with http method then responds based on method`() {
+        this.spring.register(AuthorizeRequestsByRegexConfig::class.java).autowire()
+
+        this.mockMvc.post("/onlyPostPermitted") { with(csrf()) }
+            .andExpect {
+                status { isOk }
+            }
+
+        this.mockMvc.get("/onlyPostPermitted")
+            .andExpect {
+                status { isForbidden }
+            }
+    }
+
     @EnableWebSecurity
     open class AuthorizeRequestsByRegexConfig : WebSecurityConfigurerAdapter() {
         override fun configure(http: HttpSecurity) {
             http {
                 authorizeRequests {
                     authorize(RegexRequestMatcher("/path", null), permitAll)
+                    authorize(RegexRequestMatcher("/onlyPostPermitted", "POST"), permitAll)
+                    authorize(RegexRequestMatcher("/onlyPostPermitted", "GET"), denyAll)
                     authorize(RegexRequestMatcher(".*", null), authenticated)
                 }
             }
@@ -88,6 +109,10 @@ class AuthorizeRequestsDslTests {
             @RequestMapping("/path")
             fun path() {
             }
+
+            @RequestMapping("/onlyPostPermitted")
+            fun onlyPostPermitted() {
+            }
         }
     }
 
@@ -271,4 +296,91 @@ class AuthorizeRequestsDslTests {
             }
         }
     }
+
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class AuthorizeRequestsByMvcConfigWithHttpMethod : WebSecurityConfigurerAdapter() {
+        override fun configure(http: HttpSecurity) {
+            http {
+                authorizeRequests {
+                    authorize(HttpMethod.GET, "/path", permitAll)
+                    authorize(HttpMethod.PUT, "/path", denyAll)
+                }
+            }
+        }
+
+        @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 MvcMatcherServletPathHttpMethodConfig : WebSecurityConfigurerAdapter() {
+        override fun configure(http: HttpSecurity) {
+            http {
+                authorizeRequests {
+                    authorize(HttpMethod.GET, "/path", "/spring", denyAll)
+                    authorize(HttpMethod.PUT, "/path", "/spring", denyAll)
+                }
+            }
+        }
+
+        @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(MvcMatcherServletPathConfig::class.java).autowire()
+
+        this.mockMvc.perform(MockMvcRequestBuilders.get("/spring/path")
+            .with { request ->
+                request.apply {
+                    servletPath = "/spring"
+                }
+            })
+            .andExpect(status().isForbidden)
+
+        this.mockMvc.perform(MockMvcRequestBuilders.put("/spring/path")
+            .with { request ->
+                request.apply {
+                    servletPath = "/spring"
+                    csrf()
+                }
+            })
+            .andExpect(status().isForbidden)
+
+        this.mockMvc.perform(MockMvcRequestBuilders.get("/other/path")
+            .with { request ->
+                request.apply {
+                    servletPath = "/other"
+                }
+            })
+            .andExpect(status().isOk)
+    }
 }