From 558bb161c51bbbe5b641a576900881b35f4a3646 Mon Sep 17 00:00:00 2001 From: nor-ek Date: Tue, 26 Apr 2022 15:18:36 +0200 Subject: [PATCH] Security Context Dsl Closes gh-11039 --- .../config/annotation/web/HttpSecurityDsl.kt | 29 +++ .../annotation/web/SecurityContextDsl.kt | 42 ++++ .../annotation/web/SecurityContextDslTests.kt | 180 ++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 config/src/main/kotlin/org/springframework/security/config/annotation/web/SecurityContextDsl.kt create mode 100644 config/src/test/kotlin/org/springframework/security/config/annotation/web/SecurityContextDslTests.kt diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt index 3f73de6d18..2424d2121a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt @@ -50,6 +50,7 @@ import jakarta.servlet.http.HttpServletRequest * ``` * * @author Eleftheria Stein + * @author Norbert Nowak * @since 5.3 * @param httpConfiguration the configurations to apply to [HttpSecurity] */ @@ -905,4 +906,32 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu init() authenticationManager?.also { this.http.authenticationManager(authenticationManager) } } + + /** + * Enables security context configuration. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * securityContext { + * securityContextRepository = SECURITY_CONTEXT_REPOSITORY + * } + * } + * } + * } + * ``` + * @author Norbert Nowak + * @since 5.7 + * @param securityContextConfiguration configuration to be applied to Security Context + * @see [SecurityContextDsl] + */ + fun securityContext(securityContextConfiguration: SecurityContextDsl.() -> Unit) { + val securityContextCustomizer = SecurityContextDsl().apply(securityContextConfiguration).get() + this.http.securityContext(securityContextCustomizer) + } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/SecurityContextDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/SecurityContextDsl.kt new file mode 100644 index 0000000000..2530961e00 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/SecurityContextDsl.kt @@ -0,0 +1,42 @@ +/* + * 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.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer +import org.springframework.security.web.context.SecurityContextRepository + + +/** + * A Kotlin DSL to configure [HttpSecurity] security context using idiomatic Kotlin code. + * + * @property securityContextRepository the [SecurityContextRepository] used for persisting [org.springframework.security.core.context.SecurityContext] between requests + * @author Norbert Nowak + * @since 5.7 + */ +@SecurityMarker +class SecurityContextDsl { + + var securityContextRepository: SecurityContextRepository? = null + var requireExplicitSave: Boolean? = null + + internal fun get(): (SecurityContextConfigurer) -> Unit { + return { securityContext -> + securityContextRepository?.also { securityContext.securityContextRepository(it) } + requireExplicitSave?.also { securityContext.requireExplicitSave(it) } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/SecurityContextDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/SecurityContextDslTests.kt new file mode 100644 index 0000000000..2b9a5bed9c --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/SecurityContextDslTests.kt @@ -0,0 +1,180 @@ +/* + * 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 io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.ObjectPostProcessor +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +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 +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.userdetails.PasswordEncodedUser +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.context.* +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + +@ExtendWith(SpringTestContextExtension::class) +class SecurityContextDslTests { + + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mvc: MockMvc + + @Test + fun `configure when registering object post processor then invoked on security context persistence filter`() { + spring.register(ObjectPostProcessorConfig::class.java).autowire() + verify { ObjectPostProcessorConfig.objectPostProcessor.postProcess(any()) } + } + + @EnableWebSecurity + open class ObjectPostProcessorConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + securityContext { } + } + // @formatter:on + } + + @Bean + open fun objectPostProcessor(): ObjectPostProcessor = objectPostProcessor + + companion object { + var objectPostProcessor: ObjectPostProcessor = spyk(ReflectingObjectPostProcessor()) + + class ReflectingObjectPostProcessor : ObjectPostProcessor { + override fun postProcess(`object`: O): O = `object` + } + } + } + + @Test + fun `security context when invoked twice then uses original security context repository`() { + spring.register(DuplicateDoesNotOverrideConfig::class.java).autowire() + every { DuplicateDoesNotOverrideConfig.SECURITY_CONTEXT_REPOSITORY.loadContext(any()) } returns mockk(relaxed = true) + mvc.perform(get("/")) + verify(exactly = 1) { DuplicateDoesNotOverrideConfig.SECURITY_CONTEXT_REPOSITORY.loadContext(any()) } + } + + + @EnableWebSecurity + open class DuplicateDoesNotOverrideConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + securityContext { + securityContextRepository = SECURITY_CONTEXT_REPOSITORY + } + securityContext { } + } + // @formatter:on + } + + companion object { + var SECURITY_CONTEXT_REPOSITORY = mockk(relaxed = true) + } + } + + @Test + fun `security context when security context repository not configured then does not throw exception`() { + spring.register(SecurityContextRepositoryDefaultsSecurityContextRepositoryConfig::class.java).autowire() + assertDoesNotThrow { mvc.perform(get("/")) } + } + + @EnableWebSecurity + open class SecurityContextRepositoryDefaultsSecurityContextRepositoryConfig : WebSecurityConfigurerAdapter(true) { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + addFilterAt(WebAsyncManagerIntegrationFilter()) + anonymous { } + securityContext { } + authorizeRequests { + authorize(anyRequest, permitAll) + } + httpBasic { } + } + // @formatter:on + } + + override fun configure(auth: AuthenticationManagerBuilder) { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + // @formatter:on + } + } + + @Test + fun `security context when require explicit save is true then configure SecurityContextHolderFilter`() { + val repository = HttpSessionSecurityContextRepository() + val testContext = spring.register(RequireExplicitSaveConfig::class.java) + testContext.autowire() + val filterChainProxy = testContext.context.getBean(FilterChainProxy::class.java) + // @formatter:off + val filterTypes = filterChainProxy.getFilters("/").toList() + + assertThat(filterTypes) + .anyMatch { it is SecurityContextHolderFilter } + .noneMatch { it is SecurityContextPersistenceFilter } + // @formatter:on + val mvcResult = mvc.perform(SecurityMockMvcRequestBuilders.formLogin()).andReturn() + val securityContext = repository + .loadContext(HttpRequestResponseHolder(mvcResult.request, mvcResult.response)) + assertThat(securityContext.authentication).isNotNull + } + + @EnableWebSecurity + open class RequireExplicitSaveConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + formLogin { } + securityContext { + requireExplicitSave = true + } + } + // @formatter:on + } + + override fun configure(auth: AuthenticationManagerBuilder) { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()) + // @formatter:on + } + } +}