From 1ff5eb6b57d2ba55c82a90c2150aa760c36ec237 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 26 Jun 2023 10:52:05 -0300 Subject: [PATCH] Add with() method to apply SecurityConfigurerAdapter This method is intended to replace .apply() because it will not be possible to chain configurations when .and() gets removed Closes gh-13204 --- .../AbstractConfiguredSecurityBuilder.java | 20 +++- .../config/annotation/web/HttpSecurityDsl.kt | 38 ++++++- ...bstractConfiguredSecurityBuilderTests.java | 16 ++- .../HttpSecurityConfigurationTests.java | 66 +++++++++++ .../annotation/web/HttpSecurityDslTests.kt | 73 +++++++++++- .../pages/servlet/configuration/java.adoc | 105 ++++++++++++++++-- docs/modules/ROOT/pages/whats-new.adoc | 1 + 7 files changed, 304 insertions(+), 15 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java index 588bcb7f52a..26a6c960dea 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2023 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. @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.util.Assert; import org.springframework.web.filter.DelegatingFilterProxy; @@ -139,6 +140,23 @@ public > C apply(C configurer) throws Excepti return configurer; } + /** + * Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and + * invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}. + * @param configurer + * @return the {@link SecurityBuilder} for further customizations + * @throws Exception + * @since 6.2 + */ + @SuppressWarnings("unchecked") + public > B with(C configurer, Customizer customizer) throws Exception { + configurer.addObjectPostProcessor(this.objectPostProcessor); + configurer.setBuilder((B) this); + add(configurer); + customizer.customize(configurer); + return (B) this; + } + /** * Sets an object that is shared by multiple {@link SecurityConfigurer}. * @param sharedType the Class to key the shared object by. 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 26613434155..6d992692ef2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -16,6 +16,9 @@ package org.springframework.security.config.annotation.web +import jakarta.servlet.Filter +import jakarta.servlet.http.HttpServletRequest +import org.checkerframework.checker.units.qual.C import org.springframework.context.ApplicationContext import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.SecurityConfigurerAdapter @@ -24,9 +27,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.util.matcher.RequestMatcher -import org.springframework.util.ClassUtils -import jakarta.servlet.Filter -import jakarta.servlet.http.HttpServletRequest /** * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl]. @@ -107,6 +107,36 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu return this.http.apply(configurer).apply(configuration) } + /** + * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity] + * + * Example: + * + * ``` + * @Configuration + * @EnableWebSecurity + * class SecurityConfig { + * + * @Bean + * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + * http { + * with(CustomSecurityConfigurer()) { + * customProperty = "..." + * } + * } + * return http.build() + * } + * } + * ``` + * + * @param configurer + * the [HttpSecurity] for further customizations + * @since 6.2 + */ + fun > with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? { + return this.http.with(configurer, configuration) + } + /** * Allows configuring the [HttpSecurity] to only be invoked when matching the * provided pattern. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java index a7b55ef0216..69e153e6afc 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; @@ -149,6 +150,19 @@ public void getConfigurersWhenMultipleConfigurersThenConfigurersReturned() throw assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2); } + @Test + public void withWhenConfigurerThenConfigurerAdded() throws Exception { + this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults()); + assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1); + } + + @Test + public void withWhenDuplicateConfigurerAddedThenDuplicateConfigurerRemoved() throws Exception { + this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults()); + this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults()); + assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1); + } + private static class ApplyAndRemoveSecurityConfigurer extends SecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java index 3335815878f..53241b84936 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java @@ -47,6 +47,7 @@ import org.springframework.security.authentication.event.AbstractAuthenticationEvent; import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -63,6 +64,7 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.test.web.servlet.RequestCacheResultMatcher; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; @@ -90,6 +92,8 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -365,6 +369,27 @@ public void configureWhenCorsConfigurationSourceThenApplyCors() { assertThat(configSource).isInstanceOf(UrlBasedCorsConfigurationSource.class); } + @Test + public void configureWhenAddingCustomDslUsingWithThenApplied() throws Exception { + this.spring.register(WithCustomDslConfig.class, UserDetailsConfig.class).autowire(); + SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class); + List filters = filterChain.getFilters(); + assertThat(filters).hasAtLeastOneElementOfType(UsernamePasswordAuthenticationFilter.class); + this.mockMvc.perform(formLogin()).andExpectAll(redirectedUrl("/"), authenticated()); + } + + @Test + public void configureWhenCustomDslAddedFromFactoriesAndDisablingUsingWithThenNotApplied() throws Exception { + this.springFactoriesLoader.when( + () -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader())) + .thenReturn(List.of(new WithCustomDsl())); + this.spring.register(WithCustomDslDisabledConfig.class, UserDetailsConfig.class).autowire(); + SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class); + List filters = filterChain.getFilters(); + assertThat(filters).doesNotHaveAnyElementsOfTypes(UsernamePasswordAuthenticationFilter.class); + this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated()); + } + @RestController static class NameController { @@ -661,4 +686,45 @@ public void configure(HttpSecurity builder) { } + @Configuration + @EnableWebSecurity + static class WithCustomDslConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .with(new WithCustomDsl(), Customizer.withDefaults()) + .httpBasic(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + } + + @Configuration + @EnableWebSecurity + static class WithCustomDslDisabledConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .with(new WithCustomDsl(), (dsl) -> dsl.disable()) + .httpBasic(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + } + + static class WithCustomDsl extends AbstractHttpConfigurer { + + @Override + public void init(HttpSecurity builder) throws Exception { + builder.formLogin(Customizer.withDefaults()); + } + + } + } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt index bdb849e83a6..ed11cf15a28 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web import io.mockk.every import io.mockk.mockkObject import io.mockk.verify +import jakarta.servlet.Filter import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -55,7 +56,6 @@ import org.springframework.test.web.servlet.get 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 /** * Tests for [HttpSecurityDsl] @@ -530,6 +530,18 @@ class HttpSecurityDslTests { ) } + @Test + fun `HTTP security when apply custom security configurer using with then custom filter added to filter chain`() { + this.spring.register(CustomSecurityConfigurerConfig::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filterClasses: List> = filterChain.getFilters("/").map { it.javaClass } + + assertThat(filterClasses).contains( + CustomFilter::class.java + ) + } + @Configuration @EnableWebSecurity @EnableWebMvc @@ -545,6 +557,21 @@ class HttpSecurityDslTests { } } + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class CustomSecurityConfigurerUsingWithConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + with(CustomSecurityConfigurer()) { + filter = CustomFilter() + } + } + return http.build() + } + } + class CustomSecurityConfigurer> : AbstractHttpConfigurer, H>() { var filter: Filter? = null override fun init(builder: H) { @@ -555,4 +582,46 @@ class HttpSecurityDslTests { builder.addFilterBefore(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) } } + + @Test + fun `HTTP security when apply form login using with from custom security configurer then filter added to filter chain`() { + this.spring.register(CustomDslUsingWithConfig::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filterClasses: List> = filterChain.getFilters("/").map { it.javaClass } + + assertThat(filterClasses).contains( + UsernamePasswordAuthenticationFilter::class.java + ) + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class CustomDslUsingWithConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + with(CustomDslFormLogin()) { + formLogin = true + } + httpBasic { } + } + return http.build() + } + } + + class CustomDslFormLogin: AbstractHttpConfigurer() { + + var formLogin = false + + override fun init(builder: HttpSecurity) { + if (formLogin) { + builder.formLogin { } + } + } + + } + + } diff --git a/docs/modules/ROOT/pages/servlet/configuration/java.adoc b/docs/modules/ROOT/pages/servlet/configuration/java.adoc index fc036418686..edb13ea60e9 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/java.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/java.adoc @@ -227,7 +227,11 @@ This configuration is considered after `apiFilterChain`, since it has an `@Order You can provide your own custom DSLs in Spring Security: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- public class MyCustomDsl extends AbstractHttpConfigurer { private boolean flag; @@ -260,6 +264,38 @@ public class MyCustomDsl extends AbstractHttpConfigurer() { + var flag: Boolean = false + + override fun init(http: HttpSecurity) { + // any method that adds another configurer + // must be done in the init method + http.csrf().disable() + } + + override fun configure(http: HttpSecurity) { + val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java) + + // here we lookup from the ApplicationContext. You can also just create a new instance. + val myFilter: MyFilter = context.getBean(MyFilter::class.java) + myFilter.setFlag(flag) + http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter::class.java) + } + + companion object { + @JvmStatic + fun customDsl(): MyCustomDsl { + return MyCustomDsl() + } + } +} +---- +====== + [NOTE] ==== This is actually how methods like `HttpSecurity.authorizeRequests()` are implemented. @@ -267,7 +303,11 @@ This is actually how methods like `HttpSecurity.authorizeRequests()` are impleme You can then use the custom DSL: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- @Configuration @EnableWebSecurity @@ -275,15 +315,37 @@ public class Config { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .apply(customDsl()) + .with(MyCustomDsl.customDsl(), (dsl) -> dsl .flag(true) - .and() - ...; + ) + // ... return http.build(); } } ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableWebSecurity +class Config { + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http + .with(MyCustomDsl.customDsl()) { + flag = true + } + // ... + + return http.build() + } +} +---- +====== + The code is invoked in the following order: * Code in the `Config.configure` method is invoked @@ -301,21 +363,50 @@ org.springframework.security.config.annotation.web.configurers.AbstractHttpConfi You can also explicit disable the default: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- + @Configuration @EnableWebSecurity public class Config { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .apply(customDsl()).disable() + .with(MyCustomDsl.customDsl(), (dsl) -> dsl + .disable() + ) ...; return http.build(); } } ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableWebSecurity +class Config { + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http + .with(MyCustomDsl.customDsl()) { + disable() + } + // ... + return http.build() + } + +} +---- +====== + [[post-processing-configured-objects]] == Post Processing Configured Objects diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 8f655af1233..e45a17fa746 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -7,3 +7,4 @@ Below are the highlights of the release. == Configuration * https://github.com/spring-projects/spring-security/issues/5011[gh-5011] - xref:servlet/integrations/cors.adoc[(docs)] Automatically enable `.cors()` if `CorsConfigurationSource` bean is present +* https://github.com/spring-projects/spring-security/issues/13204[gh-13204] - xref:servlet/integrations/cors.adoc[(docs)] Add `AbstractConfiguredSecurityBuilder.with(...)` method to apply configurers returning the builder