Add authorizeHttpRequests to Kotlin DSL

Closes gh-10481
This commit is contained in:
Yuriy Savchenko 2022-03-13 12:06:24 +03:00 committed by Josh Cummings
parent 932ff4f5c4
commit ca00b1415b
5 changed files with 1063 additions and 31 deletions

View File

@ -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.annotation.web
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
}

View File

@ -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.annotation.web
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)
}
}
}
}
}
}
}

View File

@ -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.
*

View File

@ -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.annotation.web
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()
}
}
}

View File

@ -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 jakarta.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()