diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt index 901f9862e4..9e35287b5f 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt +++ b/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) diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt index 3fa8b30501..eb0887d7bf 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt +++ b/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) } } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt index c4508ba8b3..d96b57af24 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt +++ b/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) + } }