diff --git a/config/src/main/java/org/springframework/security/config/ThrowingCustomizer.java b/config/src/main/java/org/springframework/security/config/ThrowingCustomizer.java new file mode 100644 index 0000000000..b6ca2987da --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ThrowingCustomizer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-present 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; + +/** + * A {@link Customizer} that allows invocation of code that throws a checked exception. + * + * @param The type of input. + */ +@FunctionalInterface +public interface ThrowingCustomizer extends Customizer { + + /** + * Default {@link Customizer#customize(Object)} that wraps any thrown checked + * exceptions (by default in a {@link RuntimeException}). + * @param t the object to customize + */ + default void customize(T t) { + try { + customizeWithException(t); + } + catch (RuntimeException ex) { + throw ex; + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Performs the customization on the given object, possibly throwing a checked + * exception. + * @param t the object to customize + * @throws Exception on error + */ + void customizeWithException(T t) throws Exception; + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 934d1b0559..ad641ea656 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -16,19 +16,24 @@ package org.springframework.security.config.annotation.web.configuration; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; +import org.springframework.security.config.Customizer; import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -46,6 +51,7 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingSupplier; import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; @@ -131,6 +137,8 @@ class HttpSecurityConfiguration { // @formatter:on applyCorsIfAvailable(http); applyDefaultConfigurers(http); + applyHttpSecurityCustomizers(this.context, http); + applyTopLevelCustomizers(this.context, http); return http; } @@ -160,6 +168,73 @@ class HttpSecurityConfiguration { } } + /** + * Applies all {@code Customizer} Bean instances to the + * {@link HttpSecurity} instance. + * @param applicationContext the {@link ApplicationContext} to lookup Bean instances + * @param http the {@link HttpSecurity} to apply the Beans to. + */ + private void applyHttpSecurityCustomizers(ApplicationContext applicationContext, HttpSecurity http) { + ResolvableType httpSecurityCustomizerType = ResolvableType.forClassWithGenerics(Customizer.class, + HttpSecurity.class); + ObjectProvider> customizerProvider = this.context + .getBeanProvider(httpSecurityCustomizerType); + + // @formatter:off + customizerProvider.orderedStream().forEach((customizer) -> + customizer.customize(http) + ); + // @formatter:on + } + + /** + * Applies all {@link Customizer} Beans to {@link HttpSecurity}. For each public, + * non-static method in HttpSecurity that accepts a Customizer + *
    + *
  • Use the {@link MethodParameter} (this preserves generics) to resolve all Beans + * for that type
  • + *
  • For each {@link Customizer} Bean invoke the {@link java.lang.reflect.Method} + * with the {@link Customizer} Bean as the argument
  • + *
+ * @param context the {@link ApplicationContext} + * @param http the {@link HttpSecurity} + * @throws Exception + */ + private void applyTopLevelCustomizers(ApplicationContext context, HttpSecurity http) { + ReflectionUtils.MethodFilter isCustomizerMethod = (method) -> { + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + if (!method.canAccess(http)) { + return false; + } + if (method.getParameterCount() != 1) { + return false; + } + if (method.getParameterTypes()[0] == Customizer.class) { + return true; + } + return false; + }; + ReflectionUtils.MethodCallback invokeWithEachCustomizerBean = (customizerMethod) -> { + + MethodParameter customizerParameter = new MethodParameter(customizerMethod, 0); + ResolvableType customizerType = ResolvableType.forMethodParameter(customizerParameter); + ObjectProvider customizerProvider = context.getBeanProvider(customizerType); + + // @formatter:off + customizerProvider.orderedStream().forEach((customizer) -> + ReflectionUtils.invokeMethod(customizerMethod, http, customizer) + ); + // @formatter:on + + }; + ReflectionUtils.doWithMethods(HttpSecurity.class, invokeWithEachCustomizerBean, isCustomizerMethod); + } + private Map, Object> createSharedObjects() { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index b77da7a721..1bff8f5dcb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.reactive; +import java.lang.reflect.Modifier; import java.util.Map; import org.springframework.beans.BeansException; @@ -28,10 +29,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ResolvableType; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker; +import org.springframework.security.config.Customizer; import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; @@ -40,6 +44,7 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver; +import org.springframework.util.ReflectionUtils; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; @@ -154,6 +159,83 @@ class ServerHttpSecurityConfiguration { @Bean(HTTPSECURITY_BEAN_NAME) @Scope("prototype") + ServerHttpSecurity httpSecurity(ApplicationContext context) { + ServerHttpSecurity http = httpSecurity(); + applyServerHttpSecurityCustomizers(context, http); + applyTopLevelBeanCustomizers(context, http); + return http; + } + + /** + * Applies all {@code Custmizer} Beans to + * {@link ServerHttpSecurity}. + * @param context the {@link ApplicationContext} + * @param http the {@link ServerHttpSecurity} + * @throws Exception + */ + private void applyServerHttpSecurityCustomizers(ApplicationContext context, ServerHttpSecurity http) { + ResolvableType httpSecurityCustomizerType = ResolvableType.forClassWithGenerics(Customizer.class, + ServerHttpSecurity.class); + ObjectProvider> customizerProvider = context + .getBeanProvider(httpSecurityCustomizerType); + + // @formatter:off + customizerProvider.orderedStream().forEach((customizer) -> + customizer.customize(http) + ); + // @formatter:on + } + + /** + * Applies all {@link Customizer} Beans to top level {@link ServerHttpSecurity} + * method. + * + * For each public, non-static method in ServerHttpSecurity that accepts a Customizer + *
    + *
  • Use the {@link MethodParameter} (this preserves generics) to resolve all Beans + * for that type
  • + *
  • For each {@link Customizer} Bean invoke the {@link java.lang.reflect.Method} + * with the {@link Customizer} Bean as the argument
  • + *
+ * @param context the {@link ApplicationContext} + * @param http the {@link ServerHttpSecurity} + * @throws Exception + */ + private void applyTopLevelBeanCustomizers(ApplicationContext context, ServerHttpSecurity http) { + ReflectionUtils.MethodFilter isCustomizerMethod = (method) -> { + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + if (!method.canAccess(http)) { + return false; + } + if (method.getParameterCount() != 1) { + return false; + } + if (method.getParameterTypes()[0] == Customizer.class) { + return true; + } + return false; + }; + ReflectionUtils.MethodCallback invokeWithEachCustomizerBean = (customizerMethod) -> { + + MethodParameter customizerParameter = new MethodParameter(customizerMethod, 0); + ResolvableType customizerType = ResolvableType.forMethodParameter(customizerParameter); + ObjectProvider customizerProvider = context.getBeanProvider(customizerType); + + // @formatter:off + customizerProvider.orderedStream().forEach((customizer) -> + ReflectionUtils.invokeMethod(customizerMethod, http, customizer) + ); + // @formatter:on + + }; + ReflectionUtils.doWithMethods(ServerHttpSecurity.class, invokeWithEachCustomizerBean, isCustomizerMethod); + } + ServerHttpSecurity httpSecurity() { ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity(); // @formatter:off diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 3d1cf0d486..847af8e364 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1316,6 +1316,10 @@ public class ServerHttpSecurity { return this.context.getBeanNamesForType(beanClass); } + ApplicationContext getApplicationContext() { + return this.context; + } + protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } 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 55e0225d65..2fecffb7b8 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 @@ -18,14 +18,22 @@ package org.springframework.security.config.annotation.web import jakarta.servlet.Filter import jakarta.servlet.http.HttpServletRequest +import org.springframework.beans.factory.ObjectProvider import org.springframework.context.ApplicationContext +import org.springframework.core.MethodParameter +import org.springframework.core.ResolvableType +import org.springframework.core.annotation.AnnotationUtils import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.Customizer import org.springframework.security.config.annotation.SecurityConfigurerAdapter import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository 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.ReflectionUtils +import java.lang.reflect.Method +import java.lang.reflect.Modifier /** * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl]. @@ -77,6 +85,117 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu var authenticationManager: AuthenticationManager? = null val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java) + init { + applyFunction1HttpSecurityDslBeans(this.context, this) + applyTopLevelFunction1SecurityDslBeans(this.context, this) + } + + + /** + * Applies all `Function1` Beans which + * allows exposing the DSL as Beans to be applied. + * + * ``` + * @Bean + * fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit { + * return { + * headers { + * contentSecurityPolicy { + * policyDirectives = "object-src 'none'" + * } + * } + * redirectToHttps { } + * } + * } + * ``` + */ + private fun applyFunction1HttpSecurityDslBeans(context: ApplicationContext, http: HttpSecurityDsl) : Unit { + val httpSecurityDslFnType = ResolvableType.forClassWithGenerics(Function1::class.java, + HttpSecurityDsl::class.java, Unit::class.java) + val httpSecurityDslFnProvider = context + .getBeanProvider>(httpSecurityDslFnType) + + // @formatter:off + httpSecurityDslFnProvider.orderedStream().forEach { fn -> fn.invoke(http) } + // @formatter:on + } + + /** + * Applies all `Function1` Beans such that `T` is a top level + * DSL on `HttpSecurityDsl`. This allows exposing the top level + * DSLs as Beans to be applied. + * + * + * ``` + * @Bean + * fun httpSecurityCustomizer(): ThrowingCustomizer { + * return ThrowingCustomizer { http -> http + * .headers { headers -> headers + * .contentSecurityPolicy { csp -> csp + * .policyDirectives("object-src 'none'") + * } + * } + * .redirectToHttps(Customizer.withDefaults()) + * } + * } + * ``` + * + * @param context the [ApplicationContext] + * @param http the [HttpSecurity] + * @throws Exception + */ + private fun applyTopLevelFunction1SecurityDslBeans(context: ApplicationContext, http: HttpSecurityDsl) { + val isCustomizerMethod = ReflectionUtils.MethodFilter { method: Method -> + if (Modifier.isStatic(method.modifiers)) { + return@MethodFilter false + } + if (!Modifier.isPublic(method.modifiers)) { + return@MethodFilter false + } + if (!method.canAccess(http)) { + return@MethodFilter false + } + if (method.parameterCount != 1) { + return@MethodFilter false + } + return@MethodFilter extractDslType(method) != null + } + val invokeWithEachDslBean = ReflectionUtils.MethodCallback { dslMethod: Method -> + val dslFunctionType = firstMethodResolvableType(dslMethod)!! + val dslFunctionProvider: ObjectProvider<*> = context.getBeanProvider(dslFunctionType) + + // @formatter:off + dslFunctionProvider.orderedStream().forEach {customizer: Any -> ReflectionUtils.invokeMethod(dslMethod, http, customizer)} + } + ReflectionUtils.doWithMethods(HttpSecurityDsl::class.java, invokeWithEachDslBean, isCustomizerMethod) + } + + /** + * From a `Method` with the first argument `Function` return `T` or `null` + * if the first argument is not a `Function`. + * @return From a `Method` with the first argument `Function` return `T`. + */ + private fun extractDslType(method: Method): ResolvableType? { + val functionType = firstMethodResolvableType(method) + if (!Function::class.java.isAssignableFrom(functionType.toClass())) { + return null + } + val functionInputType = functionType.getGeneric(0) + val securityMarker = AnnotationUtils.findAnnotation(functionInputType.toClass(), SecurityMarker::class.java) + val isSecurityDsl = securityMarker != null + if (!isSecurityDsl) { + return null + } + return functionInputType + } + + private fun firstMethodResolvableType(method: Method): ResolvableType { + val parameter = MethodParameter( + method, 0 + ) + return ResolvableType.forMethodParameter(parameter) + } + /** * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity] * diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt index 3fccaeb308..78d5054b5f 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt @@ -16,13 +16,23 @@ package org.springframework.security.config.web.server +import org.springframework.beans.factory.ObjectProvider +import org.springframework.context.ApplicationContext +import org.springframework.core.MethodParameter +import org.springframework.core.ResolvableType +import org.springframework.core.annotation.AnnotationUtils import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.context.ServerSecurityContextRepository import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.util.ReflectionUtils import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.WebFilter +import java.lang.reflect.Method +import java.lang.reflect.Modifier /** * Configures [ServerHttpSecurity] using a [ServerHttpSecurity Kotlin DSL][ServerHttpSecurityDsl]. @@ -68,6 +78,115 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in var authenticationManager: ReactiveAuthenticationManager? = null var securityContextRepository: ServerSecurityContextRepository? = null + init { + applyFunction1HttpSecurityDslBeans(this.http.applicationContext, this) + applyTopLevelFunction1SecurityDslBeans(this.http.applicationContext, this) + } + + /** + * Applies all `Function1` Beans which + * allows exposing the DSL as Beans to be applied. + * + * ``` + * @Bean + * @Order(Ordered.LOWEST_PRECEDENCE) + * fun userAuthorization(): ServerHttpSecurityDsl.() -> Unit { + * // @formatter:off + * return { + * authorizeExchange { + * authorize("/user/profile", hasRole("USER")) + * } + * } + * // @formatter:on + * } + * ``` + */ + private fun applyFunction1HttpSecurityDslBeans(context: ApplicationContext, http: ServerHttpSecurityDsl) : Unit { + val httpSecurityDslFnType = ResolvableType.forClassWithGenerics(Function1::class.java, + ServerHttpSecurityDsl::class.java, Unit::class.java) + val httpSecurityDslFnProvider = context + .getBeanProvider>(httpSecurityDslFnType) + + // @formatter:off + httpSecurityDslFnProvider.orderedStream().forEach { fn -> fn.invoke(http) } + // @formatter:on + } + + /** + * Applies all `Function1` Beans such that `T` is a top level + * DSL on `ServerHttpSecurityDsl`. This allows exposing the top level + * DSLs as Beans to be applied. + * + * ``` + * @Bean + * fun headersSecurity(): Customizer { + * // @formatter:off + * return Customizer { headers -> headers + * .contentSecurityPolicy { csp -> csp + * .policyDirectives("object-src 'none'") + * } + * } + * // @formatter:on + * } + * ``` + * + * @param context the [ApplicationContext] + * @param http the [HttpSecurity] + * @throws Exception + */ + private fun applyTopLevelFunction1SecurityDslBeans(context: ApplicationContext, http: ServerHttpSecurityDsl) { + val isCustomizerMethod = ReflectionUtils.MethodFilter { method: Method -> + if (Modifier.isStatic(method.modifiers)) { + return@MethodFilter false + } + if (!Modifier.isPublic(method.modifiers)) { + return@MethodFilter false + } + if (!method.canAccess(http)) { + return@MethodFilter false + } + if (method.parameterCount != 1) { + return@MethodFilter false + } + return@MethodFilter extractDslType(method) != null + } + + val invokeWithEachDslBean = ReflectionUtils.MethodCallback { dslMethod: Method -> + val dslFunctionType = firstMethodResolvableType(dslMethod)!! + val dslFunctionProvider: ObjectProvider<*> = context.getBeanProvider(dslFunctionType) + + // @formatter:off + dslFunctionProvider.orderedStream().forEach {customizer: Any -> ReflectionUtils.invokeMethod(dslMethod, http, customizer)} + } + ReflectionUtils.doWithMethods(ServerHttpSecurityDsl::class.java, invokeWithEachDslBean, isCustomizerMethod) + } + + /** + * From a `Method` with the first argument `Function` return `T` or `null` + * if the first argument is not a `Function`. + * @return From a `Method` with the first argument `Function` return `T`. + */ + private fun extractDslType(method: Method): ResolvableType? { + val functionType = firstMethodResolvableType(method) + if (!Function::class.java.isAssignableFrom(functionType.toClass())) { + return null + } + val functionInputType = functionType.getGeneric(0) + val securityMarker = AnnotationUtils.findAnnotation(functionInputType.toClass(), ServerSecurityMarker::class.java) + val isSecurityDsl = securityMarker != null + if (!isSecurityDsl) { + return null + } + return functionInputType + } + + private fun firstMethodResolvableType(method: Method): ResolvableType { + val parameter = MethodParameter( + method, 0 + ) + return ResolvableType.forMethodParameter(parameter) + } + /** * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the * provided [ServerWebExchangeMatcher]. 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 e057b30185..6aac552970 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 @@ -28,14 +28,20 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.event.EventListener; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.access.AccessDeniedException; @@ -54,6 +60,7 @@ import org.springframework.security.config.annotation.SecurityContextChangedList import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -82,12 +89,15 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; @@ -425,6 +435,77 @@ public class HttpSecurityConfigurationTests { .andExpectAll(status().isFound(), redirectedUrl("/"), authenticated()); } + @Test + void authorizeHttpRequestsCustomizerBean() throws Exception { + this.spring.register(AuthorizeRequestsBeanConfiguration.class, UserDetailsConfig.class).autowire(); + + Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests = this.spring + .getContext() + .getBean("authorizeRequests", Customizer.class); + + verify(authorizeRequests).customize(any()); + + } + + @Test + void multiAuthorizeHttpRequestsCustomizerBean() throws Exception { + this.spring.register(MultiAuthorizeRequestsBeanConfiguration.class, UserDetailsConfig.class).autowire(); + + Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests0 = this.spring + .getContext() + .getBean("authorizeRequests0", Customizer.class); + Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests = this.spring + .getContext() + .getBean("authorizeRequests", Customizer.class); + InOrder inOrder = Mockito.inOrder(authorizeRequests0, authorizeRequests); + + ArgumentCaptor.AuthorizationManagerRequestMatcherRegistry> arg0 = ArgumentCaptor + .forClass(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry.class); + ArgumentCaptor.AuthorizationManagerRequestMatcherRegistry> arg1 = ArgumentCaptor + .forClass(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry.class); + inOrder.verify(authorizeRequests0).customize(arg0.capture()); + inOrder.verify(authorizeRequests).customize(arg1.capture()); + } + + @Test + void disableAuthorizeHttpRequestsCustomizerBean() throws Exception { + this.spring.register(AuthorizeRequestsBeanConfiguration.class, UserDetailsConfig.class).autowire(); + + Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests = this.spring + .getContext() + .getBean("authorizeRequests", Customizer.class); + + verify(authorizeRequests).customize(any()); + + } + + @Test + void httpSecurityCustomizerBean() throws Exception { + this.spring.register(HttpSecurityCustomizerBeanConfiguration.class, UserDetailsConfig.class).autowire(); + + Customizer httpSecurityCustomizer = this.spring.getContext() + .getBean("httpSecurityCustomizer", Customizer.class); + + ArgumentCaptor arg0 = ArgumentCaptor.forClass(HttpSecurity.class); + verify(httpSecurityCustomizer).customize(arg0.capture()); + } + + @Test + void multiHttpSecurityCustomizerBean() throws Exception { + this.spring.register(MultiHttpSecurityCustomizerBeanConfiguration.class, UserDetailsConfig.class).autowire(); + + Customizer httpSecurityCustomizer = this.spring.getContext() + .getBean("httpSecurityCustomizer", Customizer.class); + Customizer httpSecurityCustomizer0 = this.spring.getContext() + .getBean("httpSecurityCustomizer0", Customizer.class); + InOrder inOrder = Mockito.inOrder(httpSecurityCustomizer0, httpSecurityCustomizer); + + ArgumentCaptor arg0 = ArgumentCaptor.forClass(HttpSecurity.class); + ArgumentCaptor arg1 = ArgumentCaptor.forClass(HttpSecurity.class); + inOrder.verify(httpSecurityCustomizer0).customize(arg0.capture()); + inOrder.verify(httpSecurityCustomizer).customize(arg1.capture()); + } + @RestController static class NameController { @@ -785,6 +866,134 @@ public class HttpSecurityConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @EnableWebMvc + static class AuthorizeRequestsBeanConfiguration { + + @Bean + SecurityFilterChain noAuthorizeSecurity(HttpSecurity http) throws Exception { + http.httpBasic(withDefaults()); + return http.build(); + } + + @Bean + static Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests() + throws Exception { + Customizer.AuthorizationManagerRequestMatcherRegistry> authz = mock( + Customizer.class, withSettings().name("authz")); + // prevent validation errors of no authorization rules being defined + willAnswer(((invocation) -> { + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry requests = invocation + .getArgument(0); + requests.anyRequest().authenticated(); + return null; + })).given(authz).customize(any()); + return authz; + } + + @RestController + static class PublicController { + + @GetMapping("/public") + String permitAll() { + return "public"; + } + + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @EnableWebMvc + static class DisableAuthorizeRequestsBeanConfiguration { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + http.httpBasic(withDefaults()); + // @formatter:off + http.authorizeHttpRequests((requests) -> requests + .anyRequest().permitAll() + ); + // @formatter:on + return http.build(); + } + + @Bean + static Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests() + throws Exception { + // @formatter:off + return (requests) -> requests + .anyRequest().denyAll(); + // @formatter:on + } + + @RestController + static class PublicController { + + @GetMapping("/public") + String permitAll() { + return "public"; + } + + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(AuthorizeRequestsBeanConfiguration.class) + static class MultiAuthorizeRequestsBeanConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + static Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeRequests0() + throws Exception { + return mock(Customizer.class, withSettings().name("authz0")); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @EnableWebMvc + static class HttpSecurityCustomizerBeanConfiguration { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + http.httpBasic(withDefaults()); + return http.build(); + } + + @Bean + static Customizer httpSecurityCustomizer() { + return mock(Customizer.class, withSettings().name("httpSecurityCustomizer")); + } + + @RestController + static class PublicController { + + @GetMapping("/public") + String permitAll() { + return "public"; + } + + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(HttpSecurityCustomizerBeanConfiguration.class) + static class MultiHttpSecurityCustomizerBeanConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + static Customizer httpSecurityCustomizer0() throws Exception { + return mock(Customizer.class, withSettings().name("httpSecurityCustomizer0")); + } + + } + private static class TestCompromisedPasswordChecker implements CompromisedPasswordChecker { @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java index 63df149884..b68736ced4 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java @@ -29,12 +29,17 @@ import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mockito; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.password.CompromisedPasswordDecision; @@ -46,6 +51,7 @@ import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -267,6 +273,47 @@ public class ServerHttpSecurityConfigurationTests { assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain after"); } + @Test + void authorizeExchangeCustomizerBean() { + this.spring.register(AuthorizeExchangeCustomizerBeanConfig.class).autowire(); + Customizer authzCustomizer = this.spring.getContext().getBean("authz", Customizer.class); + + ArgumentCaptor arg0 = ArgumentCaptor.forClass(AuthorizeExchangeSpec.class); + verify(authzCustomizer).customize(arg0.capture()); + } + + @Test + void multiAuthorizeExchangeCustomizerBean() { + this.spring.register(MultiAuthorizeExchangeCustomizerBeanConfig.class).autowire(); + Customizer authzCustomizer = this.spring.getContext().getBean("authz", Customizer.class); + + ArgumentCaptor arg0 = ArgumentCaptor.forClass(AuthorizeExchangeSpec.class); + verify(authzCustomizer).customize(arg0.capture()); + } + + @Test + void serverHttpSecurityCustomizerBean() { + this.spring.register(ServerHttpSecurityCustomizerConfig.class).autowire(); + Customizer httpSecurityCustomizer = this.spring.getContext() + .getBean("httpSecurityCustomizer", Customizer.class); + + ArgumentCaptor arg0 = ArgumentCaptor.forClass(ServerHttpSecurity.class); + verify(httpSecurityCustomizer).customize(arg0.capture()); + } + + @Test + void multiServerHttpSecurityCustomizerBean() { + this.spring.register(MultiServerHttpSecurityCustomizerConfig.class).autowire(); + Customizer httpSecurityCustomizer = this.spring.getContext() + .getBean("httpSecurityCustomizer", Customizer.class); + Customizer httpSecurityCustomizer0 = this.spring.getContext() + .getBean("httpSecurityCustomizer0", Customizer.class); + InOrder inOrder = Mockito.inOrder(httpSecurityCustomizer0, httpSecurityCustomizer); + ArgumentCaptor arg0 = ArgumentCaptor.forClass(ServerHttpSecurity.class); + inOrder.verify(httpSecurityCustomizer0).customize(arg0.capture()); + inOrder.verify(httpSecurityCustomizer).customize(arg0.capture()); + } + @Configuration static class SubclassConfig extends ServerHttpSecurityConfiguration { @@ -474,4 +521,64 @@ public class ServerHttpSecurityConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @EnableWebFlux + @EnableWebFluxSecurity + @Import(UserDetailsConfig.class) + static class AuthorizeExchangeCustomizerBeanConfig { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + return http.build(); + } + + @Bean + static Customizer authz() { + return mock(Customizer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(AuthorizeExchangeCustomizerBeanConfig.class) + static class MultiAuthorizeExchangeCustomizerBeanConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + Customizer authz0() { + return mock(Customizer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebFlux + @EnableWebFluxSecurity + @Import(UserDetailsConfig.class) + static class ServerHttpSecurityCustomizerConfig { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + return http.build(); + } + + @Bean + static Customizer httpSecurityCustomizer() { + return mock(Customizer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(ServerHttpSecurityCustomizerConfig.class) + static class MultiServerHttpSecurityCustomizerConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + static Customizer httpSecurityCustomizer0() { + return mock(Customizer.class); + } + + } + } 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 e1c55fe50a..4abd82e745 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 @@ -623,5 +623,38 @@ class HttpSecurityDslTests { } + @Test + fun `HTTP security when Dsl Bean`() { + this.spring.register(DslBeanConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { + string("Content-Security-Policy", "object-src 'none'") + } + } + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class DslBeanConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + httpBasic { } + } + return http.build() + } + + @Bean + open fun headersDsl(): HeadersDsl.() -> Unit { + return { + contentSecurityPolicy { + policyDirectives = "object-src 'none'" + } + } + } + } } diff --git a/docs/modules/ROOT/pages/reactive/configuration/webflux.adoc b/docs/modules/ROOT/pages/reactive/configuration/webflux.adoc index ad17e3093a..7383a1cddb 100644 --- a/docs/modules/ROOT/pages/reactive/configuration/webflux.adoc +++ b/docs/modules/ROOT/pages/reactive/configuration/webflux.adoc @@ -242,3 +242,72 @@ It matches the requests in order by the `securityMatcher` definition. In this case, that means that, if the URL path starts with `/api`, Spring Security uses `apiHttpSecurity`. If the URL does not start with `/api`, Spring Security defaults to `webHttpSecurity`, which has an implied `securityMatcher` that matches any request. + +[[modular-serverhttpsecurity-configuration]] +== Modular ServerHttpSecurity Configuration + +Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it within the `SecurityWebFilterChain` Bean declaration. +However, there are times that users may want to modularize the configuration. +This can be done using: + +* xref:#serverhttpsecurity-customizer-bean[Customizer Beans] +* xref:#top-level-customizer-bean[Top Level ServerHttpSecurity Customizer Beans] + +// FIXME: this needs to link to appropriate spot +// NOTE: If you are using Spring Security's xref:servlet/configuration/kotlin.adoc[], then you can also expose `*Dsl -> Unit` Beans as outlined in xref:./kotlin.adoc#modular-httpsecuritydsl-configuration[Modular HttpSecurityDsl Configuration]. + + +[[serverhttpsecurity-customizer-bean]] +=== Customizer Beans + +If you would like to modularize your security configuration you can place logic in a `Customizer` Bean. +For example, the following configuration will ensure all `ServerHttpSecurity` instances are configured to: + +include-code::./ServerHttpSecurityCustomizerBeanConfiguration[tag=httpSecurityCustomizer,indent=0] + +<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'` +<2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https] + + +[[top-level-customizer-bean]] +=== Top Level ServerHttpSecurity Customizer Beans + +If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level `HttpSecurity` `Customizer` Beans. + +A top level `HttpSecurity` `Customizer` type can be summarized as any `Customizer` that matches `public HttpSecurity.*(Customizer)`. +This translates to any `Customizer` that is a single argument to a public method on javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[]. + +A few examples can help to clarify. +If `Customizer` is published as a Bean, it will not be be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.configurers.HeadersConfigurer#contentTypeOptions(org.springframework.security.config.Customizer)[] which is not a method defined on `HttpSecurity`. +However, if `Customizer>` is published as a Bean, it will be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity#headers(org.springframework.security.config.Customizer)[]. + +For example, the following configuration will ensure that the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] is set to `object-src 'none'`: + +include-code::./TopLevelCustomizerBeanConfiguration[tag=headersCustomizer,indent=0] + +[[customizer-bean-ordering]] +=== Customizer Bean Ordering + +First each xref:#httpsecurity-customizer-bean[Customizer Bean] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()]. +This means that if there are multiple `Customizer` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering. + +Next every xref:#top-level-customizer-bean[Top Level HttpSecurity Customizer Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`. +If there is are two `Customizer>` beans and two `Customizer>` instances, the order that each `Customizer` type is invoked is undefined. +However, the order that each instance of `Customizer>` is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions. + +Finally, the `HttpSecurity` Bean is injected as a Bean. +All `Customizer` instances are applied before the `HttpSecurity` Bean is created. +This allows overriding the customizations provided by the `Customizer` Beans. + +You can find an example below that illustrates the ordering: + +include-code::./CustomizerBeanOrderingConfiguration[tag=sample,indent=0] + +<1> First all `Customizer` instances are applied. +The `adminAuthorization` Bean has the highest `@Order` so it is applied first. +If there are no `@Order` annotations on the `Customizer` Beans or the `@Order` annotations had the same value, then the order that the `Customizer` instances are applied is undefined. +<2> The `userAuthorization` is applied next due to being an instance of `Customizer` +<3> The order that the `Customizer` types are undefined. +In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined. +If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `Customizer>` Beans. +<4> After all of the `Customizer` Beans are applied, the `HttpSecurity` is passed in as a Bean. diff --git a/docs/modules/ROOT/pages/servlet/configuration/java.adoc b/docs/modules/ROOT/pages/servlet/configuration/java.adoc index 193ce75c64..0ade24ccfb 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/java.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/java.adoc @@ -664,6 +664,75 @@ class Config { ---- ====== +[[modular-httpsecurity-configuration]] +== Modular HttpSecurity Configuration + +Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it in a single `SecurityFilterChain` instance. +However, there are times that users may want to modularize the configuration. +This can be done using: + +* xref:#httpsecurity-customizer-bean[Customizer Beans] +* xref:#top-level-customizer-bean[Top Level HttpSecurity Customizer Beans] + +NOTE: If you are using Spring Security's xref:servlet/configuration/kotlin.adoc[], then you can also expose `*Dsl -> Unit` Beans as outlined in xref:./kotlin.adoc#modular-httpsecuritydsl-configuration[Modular HttpSecurityDsl Configuration]. + + +[[httpsecurity-customizer-bean]] +=== Customizer Beans + +If you would like to modularize your security configuration you can place logic in a `Customizer` Bean. +For example, the following configuration will ensure all `HttpSecurity` instances are configured to: + +include-code::./HttpSecurityCustomizerBeanConfiguration[tag=httpSecurityCustomizer,indent=0] + +<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'` +<2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https] + + +[[top-level-customizer-bean]] +=== Top Level HttpSecurity Customizer Beans + +If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level `HttpSecurity` `Customizer` Beans. + +A top level `HttpSecurity` `Customizer` type can be summarized as any `Customizer` that matches `public HttpSecurity.*(Customizer)`. +This translates to any `Customizer` that is a single argument to a public method on javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[]. + +A few examples can help to clarify. +If `Customizer` is published as a Bean, it will not be be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.configurers.HeadersConfigurer#contentTypeOptions(org.springframework.security.config.Customizer)[] which is not a method defined on `HttpSecurity`. +However, if `Customizer>` is published as a Bean, it will be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity#headers(org.springframework.security.config.Customizer)[]. + +For example, the following configuration will ensure that the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] is set to `object-src 'none'`: + +include-code::./TopLevelCustomizerBeanConfiguration[tag=headersCustomizer,indent=0] + +[[customizer-bean-ordering]] +=== Customizer Bean Ordering + +First each xref:#httpsecurity-customizer-bean[Customizer Bean] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()]. +This means that if there are multiple `Customizer` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering. + +Next every xref:#top-level-customizer-bean[Top Level HttpSecurity Customizer Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`. +If there is are two `Customizer>` beans and two `Customizer>` instances, the order that each `Customizer` type is invoked is undefined. +However, the order that each instance of `Customizer>` is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions. + +Finally, the `HttpSecurity` Bean is injected as a Bean. +All `Customizer` instances are applied before the `HttpSecurity` Bean is created. +This allows overriding the customizations provided by the `Customizer` Beans. + +You can find an example below that illustrates the ordering: + +include-code::./CustomizerBeanOrderingConfiguration[tag=sample,indent=0] + +<1> First all `Customizer` instances are applied. +The `adminAuthorization` Bean has the highest `@Order` so it is applied first. +If there are no `@Order` annotations on the `Customizer` Beans or the `@Order` annotations had the same value, then the order that the `Customizer` instances are applied is undefined. +<2> The `userAuthorization` is applied next due to being an instance of `Customizer` +<3> The order that the `Customizer` types are undefined. +In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined. +If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `Customizer>` Beans. +<4> After all of the `Customizer` Beans are applied, the `HttpSecurity` is passed in as a Bean. + + [[post-processing-configured-objects]] == Post Processing Configured Objects diff --git a/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc b/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc index 6e0a3befcb..2d288f0a23 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc @@ -346,3 +346,76 @@ class BankingSecurityConfig { This configuration will handle requests not covered by the other filter chains and will be processed last (no `@Order` defaults to last). Requests that match `/`, `/user-login`, `/user-logout`, `/notices`, `/contact` and `/register` allow access without authentication. Any other requests require the user to be authenticated to access any URL not explicitly allowed or protected by other filter chains. + + +[[modular-httpsecuritydsl-configuration]] +== Modular HttpSecurityDsl Configuration + +Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it in a single `SecurityFilterChain` instance. +However, there are times that users may want to modularize the configuration. +This can be done using: + +* xref:#httpsecuritydsl-bean[HttpSecurityDsl.() -> Unit Beans] +* xref:#top-level-dsl-bean[Top Level Security Dsl Beans] + +NOTE: Since the Spring Security Kotlin Dsl (`HttpSecurityDsl`) uses `HttpSecurity`, all of the Java xref:./kotlin.adoc#modular-bean-configuration[Modular Bean Customization] is applied before xref:#modular-httpsecuritydsl-configuration[Modular HttpSecurity Configuration]. + +[[httpsecuritydsl-bean]] +=== HttpSecurityDsl.() -> Unit Beans + +If you would like to modularize your security configuration you can place logic in a `HttpSecurityDsl.() -> Unit` Bean. +For example, the following configuration will ensure all `HttpSecurityDsl` instances are configured to: + +include-code::./HttpSecurityDslBeanConfiguration[tag=httpSecurityDslBean,indent=0] + +<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'` +<2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https] + + +[[top-level-dsl-bean]] +=== Top Level Security Dsl Beans + +If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level Security Dsl Beans. + +A top level Security Dsl can be summarized as any class Dsl class that matches `public HttpSecurityDsl.*()`. +This translates to any Security Dsl that is a single argument to a public method on `HttpSecurityDsl`. + +A few examples can help to clarify. +If `ContentTypeOptionsDsl.() -> Unit` is published as a Bean, it will not be be automatically applied because it is an argument to `HeadersDsl#contentTypeOptions(ContentTypeOptionsDsl.() -> Unit)` and is not an argument to a method defined on `HttpSecurityDsl`. +However, if `HeadersDsl.() -> Unit` is published as a Bean, it will be automatically applied because it is an argument to `HttpSecurityDsl.headers(HeadersDsl.() -> Unit)`. + +For example, the following configuration ensure all `HttpSecurityDsl` instances are configured to: + +include-code::./TopLevelDslBeanConfiguration[tag=headersSecurity,indent=0] + +<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'` + +[[dsl-bean-ordering]] +=== Dsl Bean Ordering + +First, all xref:servlet/configuration/java.adoc#modular-httpsecurity-configuration[Modular HttpSecurity Configuration] is applied since the Kotlin Dsl uses an `HttpSecurity` Bean. + +Second, each xref:#httpsecuritydsl-bean[HttpSecurityDsl.() -> Unit Beans] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()]. +This means that if there are multiple `HttpSecurity.() -> Unit` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering. + +Next, every xref:#top-level-dsl-bean[Top Level Security Dsl Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`. +If there is are differt types of top level security Beans (.e.g. `HeadersDsl.() -> Unit` and `HttpsRedirectDsl.() -> Unit`), then the order that each Dsl type is invoked is undefined. +However, the order that each instance of of the same top level security Bean type is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions. + +Finally, the `HttpSecurityDsl` Bean is injected as a Bean. +All `*Dsl.() -> Unit` Beans are applied before the `HttpSecurityDsl` Bean is created. +This allows overriding the customizations provided by the `*Dsl.() -> Unit` Beans. + +You can find an example below that illustrates the ordering: + +include-code::./DslBeanOrderingConfiguration[tag=sample,indent=0] + +<1> All xref:servlet/configuration/java.adoc#modular-httpsecurity-configuration[Modular HttpSecurity Configuration] is applied since the Kotlin Dsl uses an `HttpSecurity` Bean. +<2> All `HttpSecurity.() -> Unit` instances are applied. +The `adminAuthorization` Bean has the highest `@Order` so it is applied first. +If there are no `@Order` annotations on the `HttpSecurity.() -> Unit` Beans or the `@Order` annotations had the same value, then the order that the `HttpSecurity.() -> Unit` instances are applied is undefined. +<3> The `userAuthorization` is applied next due to being an instance of `HttpSecurity.() -> Unit` +<4> The order that the `*Dsl.() -> Unit` types are undefined. +In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined. +If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `HeadersDsl.() -> Unit` Beans. +<5> After all of the `*Dsl.() -> Unit` Beans are applied, the `HttpSecurityDsl` is passed in as a Bean. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 221c8a41fd..a339e82245 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -15,6 +15,7 @@ Each section that follows will indicate the more notable removals as well as the == Config +* Support modular modular configuration in xref::servlet/configuration/java.adoc#modular-httpsecurity-configuration[Servlets] and xref::reactive/configuration/webflux.adoc#modular-serverhttpsecurity-configuration[WebFlux] * Removed `and()` from the `HttpSecurity` DSL in favor of using the lambda methods * Removed `authorizeRequests` in favor of `authorizeHttpRequests` * Simplified expression migration for `authorizeRequests` diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java new file mode 100644 index 0000000000..6df62c8e22 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java @@ -0,0 +1,99 @@ +/* + * Copyright 2004-present 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.docs.reactive.configuration.customizerbeanordering; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * + */ +@EnableWebFlux +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class CustomizerBeanOrderingConfiguration { + + // tag::sample[] + @Bean // <4> + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((exchange) -> exchange + .anyExchange().authenticated() + ); + return http.build(); + // @formatter:on + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) // <2> + Customizer userAuthorization() { + // @formatter:off + return (http) -> http + .authorizeExchange((exchange) -> exchange + .pathMatchers("/users/**").hasRole("USER") + ); + // @formatter:on + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // <1> + Customizer adminAuthorization() { + // @formatter:off + return (http) -> http + .authorizeExchange((exchange) -> exchange + .pathMatchers("/admins/**").hasRole("ADMIN") + ); + // @formatter:on + } + + // <3> + + @Bean + Customizer contentSecurityPolicy() { + // @formatter:off + return (headers) -> headers + .contentSecurityPolicy((csp) -> csp + .policyDirectives("object-src 'none'") + ); + // @formatter:on + } + + @Bean + Customizer contentTypeOptions() { + // @formatter:off + return (headers) -> headers + .contentTypeOptions(Customizer.withDefaults()); + // @formatter:on + } + + @Bean + Customizer httpsRedirect() { + // @formatter:off + return Customizer.withDefaults(); + // @formatter:on + } + // end::sample[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java new file mode 100644 index 0000000000..754479ee78 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2004-present 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.docs.reactive.configuration.customizerbeanordering; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class CustomizerBeanOrderingTests { + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private WebTestClient webTest; + + @Test + void authorizationOrdered() throws Exception { + this.spring.register( + CustomizerBeanOrderingConfiguration.class).autowire(); + // @formatter:off + this.webTest.mutateWith(mockUser("admin").roles("ADMIN")) + .get() + .uri("https://localhost/admins/1") + .exchange() + .expectStatus().isOk(); + this.webTest.mutateWith(mockUser("user").roles("USER")) + .get() + .uri("https://localhost/admins/1") + .exchange() + .expectStatus().isForbidden(); + this.webTest.mutateWith(mockUser("user").roles("USER")) + .get() + .uri("https://localhost/users/1") + .exchange() + .expectStatus().isOk(); + this.webTest.mutateWith(mockUser("user").roles("OTHER")) + .get() + .uri("https://localhost/users/1") + .exchange() + .expectStatus().isForbidden(); + this.webTest.mutateWith(mockUser("authenticated").roles("OTHER")) + .get() + .uri("https://localhost/other") + .exchange() + .expectStatus().isOk(); + // @formatter:on + } + +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.java b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.java new file mode 100644 index 0000000000..7fde90bd5f --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2004-present 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.docs.reactive.configuration.serverhttpsecuritycustomizerbean; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * + */ +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class ServerHttpSecurityCustomizerBeanConfiguration { + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((exchange) -> exchange + .anyExchange().authenticated() + ); + return http.build(); + // @formatter:on + } + + // tag::httpSecurityCustomizer[] + @Bean + Customizer httpSecurityCustomizer() { + // @formatter:off + return (http) -> http + .headers((headers) -> headers + .contentSecurityPolicy((csp) -> csp + // <1> + .policyDirectives("object-src 'none'") + ) + ) + // <2> + .redirectToHttps(Customizer.withDefaults()); + // @formatter:on + } + // end::httpSecurityCustomizer[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.java b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.java new file mode 100644 index 0000000000..80ceda5d0e --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2004-present 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.docs.reactive.configuration.serverhttpsecuritycustomizerbean; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class ServerHttpSecurityCustomizerBeanTests { + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private WebTestClient webTest; + + @Test + void httpSecurityCustomizer() throws Exception { + this.spring.register( + ServerHttpSecurityCustomizerBeanConfiguration.class).autowire(); + // @formatter:off + this.webTest + .get() + .uri("http://localhost/") + .exchange() + .expectHeader().location("https://localhost/") + .expectHeader() + .value("Content-Security-Policy", csp -> + assertThat(csp).isEqualTo("object-src 'none'") + ); + // @formatter:on + } + +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java new file mode 100644 index 0000000000..84700b89aa --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2004-present 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.docs.reactive.configuration.toplevelcustomizerbean; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * + */ +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +public class TopLevelCustomizerBeanConfiguration { + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((exchange) -> exchange + .anyExchange().authenticated() + ); + return http.build(); + // @formatter:on + } + + // tag::headersCustomizer[] + @Bean + Customizer headersSecurity() { + // @formatter:off + return (headers) -> headers + .contentSecurityPolicy((csp) -> csp + // <1> + .policyDirectives("object-src 'none'") + ); + // @formatter:on + } + // end::headersCustomizer[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java new file mode 100644 index 0000000000..5f4b418a41 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2004-present 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.docs.reactive.configuration.toplevelcustomizerbean; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +/** + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class TopLevelCustomizerBeanTests { + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private WebTestClient webTest; + + + @Test + void headersCustomizer() throws Exception { + this.spring.register(TopLevelCustomizerBeanConfiguration.class).autowire(); + // @formatter:off + this.webTest + .get() + .uri("http://localhost/") + .exchange() + .expectHeader() + .value("Content-Security-Policy", csp -> + assertThat(csp).isEqualTo("object-src 'none'") + ); + // @formatter:on + } + + private static @NotNull ResultMatcher cspIsObjectSrcNone() { + return header().string("Content-Security-Policy", "object-src 'none'"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java new file mode 100644 index 0000000000..2f40ee9354 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright 2004-present 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.docs.servlet.configuration.customizerbeanordering; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.ThrowingCustomizer; +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.configurers.HeadersConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class CustomizerBeanOrderingConfiguration { + + // tag::sample[] + @Bean // <4> + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ); + return http.build(); + // @formatter:on + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) // <2> + ThrowingCustomizer userAuthorization() { + // @formatter:off + return (http) -> http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/users/**").hasRole("USER") + ); + // @formatter:on + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // <1> + ThrowingCustomizer adminAuthorization() { + // @formatter:off + return (http) -> http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/admins/**").hasRole("ADMIN") + ); + // @formatter:on + } + + // <3> + + @Bean + Customizer> contentSecurityPolicy() { + // @formatter:off + return (headers) -> headers + .contentSecurityPolicy((csp) -> csp + .policyDirectives("object-src 'none'") + ); + // @formatter:on + } + + @Bean + Customizer> contentTypeOptions() { + // @formatter:off + return (headers) -> headers + .contentTypeOptions(Customizer.withDefaults()); + // @formatter:on + } + + @Bean + Customizer> httpsRedirect() { + // @formatter:off + return Customizer.withDefaults(); + // @formatter:on + } + // end::sample[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java new file mode 100644 index 0000000000..ef437ecae3 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2004-present 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.docs.servlet.configuration.customizerbeanordering; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class CustomizerBeanOrderingTests { + public final SpringTestContext spring = new SpringTestContext(this).mockMvcAfterSpringSecurityOk(); + + @Autowired + private MockMvc mockMvc; + + @Test + void authorizationOrdered() throws Exception { + this.spring.register( + CustomizerBeanOrderingConfiguration.class).autowire(); + // @formatter:off + this.mockMvc + .perform(get("https://localhost/admins/1").with(user("admin").roles("ADMIN"))) + .andExpect(status().isOk()); + this.mockMvc + .perform(get("https://localhost/admins/1").with(user("user").roles("USER"))) + .andExpect(status().isForbidden()); + this.mockMvc + .perform(get("https://localhost/users/1").with(user("user").roles("USER"))) + .andExpect(status().isOk()); + this.mockMvc + .perform(get("https://localhost/users/1").with(user("user").roles("OTHER"))) + .andExpect(status().isForbidden()); + this.mockMvc + .perform(get("https://localhost/other").with(user("authenticated").roles("OTHER"))) + .andExpect(status().isOk()); + // @formatter:on + } + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.java new file mode 100644 index 0000000000..3be02d93b4 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2004-present 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.docs.servlet.configuration.httpsecuritycustomizerbean; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.ThrowingCustomizer; +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.configurers.HeadersConfigurer; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +/** + * + */ +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class HttpSecurityCustomizerBeanConfiguration { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ); + return http.build(); + // @formatter:on + } + + // tag::httpSecurityCustomizer[] + @Bean + ThrowingCustomizer httpSecurityCustomizer() { + // @formatter:off + return (http) -> http + .headers((headers) -> headers + .contentSecurityPolicy((csp) -> csp + // <1> + .policyDirectives("object-src 'none'") + ) + ) + // <2> + .redirectToHttps(Customizer.withDefaults()); + // @formatter:on + } + // end::httpSecurityCustomizer[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.java new file mode 100644 index 0000000000..5f8072bf1c --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2004-present 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.docs.servlet.configuration.httpsecuritycustomizerbean; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.ThrowingCustomizer; +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.configurers.HeadersConfigurer; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +/** + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class HttpSecurityCustomizerBeanTests { + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + void httpSecurityCustomizer() throws Exception { + this.spring.register(HttpSecurityCustomizerBeanConfiguration.class).autowire(); + // @formatter:off + this.mockMvc + .perform(get("/")) + .andExpect(redirectsToHttps()); + // headers are not sent back as a part of the redirect to https, so a separate request is necessary + this.mockMvc.perform(get("https://localhost/")) + .andExpect(cspIsObjectSrcNone()); + // @formatter:on + } + + private static @NotNull ResultMatcher redirectsToHttps() { + return mvcResult -> assertThat( + mvcResult.getResponse().getRedirectedUrl()).startsWith("https://"); + } + + private static @NotNull ResultMatcher cspIsObjectSrcNone() { + return header().string("Content-Security-Policy", "object-src 'none'"); + } + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java new file mode 100644 index 0000000000..fbe428ad61 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004-present 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.docs.servlet.configuration.toplevelcustomizerbean; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.ThrowingCustomizer; +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.configurers.HeadersConfigurer; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +/** + * + */ +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class TopLevelCustomizerBeanConfiguration { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ); + return http.build(); + // @formatter:on + } + + // tag::headersCustomizer[] + @Bean + Customizer> headersSecurity() { + // @formatter:off + return (headers) -> headers + .contentSecurityPolicy((csp) -> csp + // <1> + .policyDirectives("object-src 'none'") + ); + // @formatter:on + } + // end::headersCustomizer[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java new file mode 100644 index 0000000000..3c49de63ae --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2004-present 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.docs.servlet.configuration.toplevelcustomizerbean; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.ThrowingCustomizer; +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.configurers.HeadersConfigurer; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +/** + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class TopLevelCustomizerBeanTests { + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + + @Test + void headersCustomizer() throws Exception { + this.spring.register(TopLevelCustomizerBeanConfiguration.class).autowire(); + // @formatter:off + this.mockMvc + .perform(get("/")) + .andExpect(cspIsObjectSrcNone()); + // @formatter:on + } + + private static @NotNull ResultMatcher cspIsObjectSrcNone() { + return header().string("Content-Security-Policy", "object-src 'none'"); + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt new file mode 100644 index 0000000000..87cf9c3ffb --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2004-present 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.kt.docs.reactive.configuration.customizerbeanordering + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.security.config.Customizer +import org.springframework.security.config.ThrowingCustomizer +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.anyExchange + +/** + * + */ +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +internal class CustomizerBeanOrderingConfiguration { + // tag::sample[] + @Bean // <4> + fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + // @formatter:off + http + .authorizeExchange({ exchanges -> exchanges + .anyExchange().authenticated() + }) + return http.build() + // @formatter:on + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) // <2> + fun userAuthorization(): Customizer { + // @formatter:off + return Customizer { http -> http + .authorizeExchange { exchanges -> exchanges + .pathMatchers("/users/**").hasRole("USER") + } + } + // @formatter:on + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // <1> + fun adminAuthorization(): Customizer { + // @formatter:off + return ThrowingCustomizer { http -> http + .authorizeExchange { exchanges -> exchanges + .pathMatchers("/admins/**").hasRole("ADMIN") + } + } + // @formatter:on + } + + // <3> + + @Bean + fun contentSecurityPolicy(): Customizer { + // @formatter:off + return Customizer { headers -> headers + .contentSecurityPolicy { csp -> csp + .policyDirectives("object-src 'none'") + } + } + // @formatter:on + } + + @Bean + fun contentTypeOptions(): Customizer { + // @formatter:off + return Customizer { headers -> headers + .contentTypeOptions(Customizer.withDefaults()) + } + // @formatter:on + } + + @Bean + fun httpsRedirect(): Customizer { + // @formatter:off + return Customizer.withDefaults() + // @formatter:on + } + // end::sample[] +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt new file mode 100644 index 0000000000..c141e45ac4 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2004-present 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.kt.docs.reactive.configuration.customizerbeanordering + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers + +/** + * + */ +@ExtendWith(SpringTestContextExtension::class) +class CustomizerBeanOrderingTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var webTest: WebTestClient + + @Test + fun authorizationOrdered() { + this.spring.register(CustomizerBeanOrderingConfiguration::class.java).autowire() + // @formatter:off + this.webTest.mutateWith(mockUser("admin").roles("ADMIN")) + .get() + .uri("https://localhost/admins/1") + .exchange() + .expectStatus().isOk + this.webTest.mutateWith(mockUser("user").roles("USER")) + .get() + .uri("https://localhost/admins/1") + .exchange() + .expectStatus().isForbidden + this.webTest.mutateWith(mockUser("user").roles("USER")) + .get() + .uri("https://localhost/users/1") + .exchange() + .expectStatus().isOk + this.webTest.mutateWith(mockUser("user").roles("OTHER")) + .get() + .uri("https://localhost/users/1") + .exchange() + .expectStatus().isForbidden + this.webTest.mutateWith(mockUser("other").roles("OTHER")) + .get() + .uri("https://localhost/other") + .exchange() + .expectStatus().isOk + // @formatter:on + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt new file mode 100644 index 0000000000..2691468272 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2004-present 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.kt.docs.reactive.configuration.dslbeanordering + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.* +import org.springframework.security.web.server.SecurityWebFilterChain + +/** + * + */ +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +internal class DslBeanOrderingConfiguration { + // tag::sample[] + // All of the Java Modular Configuration is applied first <1> + + @Bean // <5> + fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + // @formatter:off + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + // @formatter:on + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) // <3> + fun userAuthorization(): ServerHttpSecurityDsl.() -> Unit { + // @formatter:off + return { + authorizeExchange { + authorize("/users/**", hasRole("USER")) + } + } + // @formatter:on + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // <2> + fun adminAuthorization(): ServerHttpSecurityDsl.() -> Unit { + // @formatter:off + return { + authorizeExchange { + authorize("/admins/**", hasRole("ADMIN")) + } + } + // @formatter:on + } + + // <4> + + @Bean + fun contentSecurityPolicy(): ServerHeadersDsl.() -> Unit { + // @formatter:off + return { + contentSecurityPolicy { + policyDirectives = "object-src 'none'" + } + } + // @formatter:on + } + + @Bean + fun contentTypeOptions(): ServerHeadersDsl.() -> Unit { + // @formatter:off + return { + contentTypeOptions { } + } + // @formatter:on + } + + @Bean + fun httpsRedirect(): ServerHttpsRedirectDsl.() -> Unit { + // @formatter:off + return { } + // @formatter:on + } + // end::sample[] +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingTests.kt new file mode 100644 index 0000000000..94ead62b4c --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingTests.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2004-present 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.kt.docs.reactive.configuration.dslbeanordering + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.kt.docs.servlet.configuration.customizerbeanordering.CustomizerBeanOrderingConfiguration +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * + */ +@ExtendWith(SpringTestContextExtension::class) +class DslBeanOrderingTests { + @JvmField + val spring = SpringTestContext(this).mockMvcAfterSpringSecurityOk() + + @Autowired + lateinit var webTest: WebTestClient + + @Test + fun dslOrdered() { + this.spring.register(org.springframework.security.kt.docs.reactive.configuration.customizerbeanordering.CustomizerBeanOrderingConfiguration::class.java).autowire() + // @formatter:off + this.webTest.mutateWith(mockUser("admin").roles("ADMIN")) + .get() + .uri("https://localhost/admins/1") + .exchange() + .expectStatus().isOk + this.webTest.mutateWith(mockUser("user").roles("USER")) + .get() + .uri("https://localhost/admins/1") + .exchange() + .expectStatus().isForbidden + this.webTest.mutateWith(mockUser("user").roles("USER")) + .get() + .uri("https://localhost/users/1") + .exchange() + .expectStatus().isOk + this.webTest.mutateWith(mockUser("user").roles("OTHER")) + .get() + .uri("https://localhost/users/1") + .exchange() + .expectStatus().isForbidden + this.webTest.mutateWith(mockUser("other").roles("OTHER")) + .get() + .uri("https://localhost/other") + .exchange() + .expectStatus().isOk + // @formatter:on + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.kt new file mode 100644 index 0000000000..d19f3f284c --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.kt @@ -0,0 +1,44 @@ +package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritycustomizerbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain + +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class ServerHttpSecurityCustomizerBeanConfiguration { + + @Bean + fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + // @formatter:off + http + .authorizeExchange({ exchanges -> exchanges + .anyExchange().authenticated() + }) + return http.build() + // @formatter:on + } + + + // tag::httpSecurityCustomizer[] + @Bean + fun httpSecurityCustomizer(): Customizer { + // @formatter:off + return Customizer { http -> http + .headers { headers -> headers + .contentSecurityPolicy { csp -> csp + // <1> + .policyDirectives("object-src 'none'") + } + } + // <2> + .redirectToHttps(Customizer.withDefaults()) + } + // @formatter:on + } + // end::httpSecurityCustomizer[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.kt new file mode 100644 index 0000000000..c7a1907394 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.kt @@ -0,0 +1,37 @@ +package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritycustomizerbean + +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.reactive.server.WebTestClient +import java.util.function.Consumer + +@ExtendWith(SpringTestContextExtension::class) +class ServerHttpSecurityCustomizerBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var webTest: WebTestClient + + @Test + fun `serverhttpsecurity customizer config`() { + this.spring.register(ServerHttpSecurityCustomizerBeanConfiguration::class.java).autowire() + // @formatter:off + this.webTest + .get() + .uri("http://localhost/") + .exchange() + .expectHeader().location("https://localhost/") + .expectHeader() + .value("Content-Security-Policy", Consumer { csp -> + assertThat(csp).isEqualTo("object-src 'none'") + }) + // @formatter:on + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanConfiguration.kt new file mode 100644 index 0000000000..0e932c40c4 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanConfiguration.kt @@ -0,0 +1,42 @@ +package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritydslbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.ServerHttpSecurityDsl +import org.springframework.security.config.web.server.invoke +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class ServerHttpSecurityDslBeanConfiguration { + + @Bean + fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + + // tag::httpSecurityDslBean[] + @Bean + fun httpSecurityDslBean(): ServerHttpSecurityDsl.() -> Unit { + return { + headers { + contentSecurityPolicy { + // <1> + policyDirectives = "object-src 'none'" + } + } + // <2> + redirectToHttps { } + } + } + // end::httpSecurityDslBean[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanTests.kt new file mode 100644 index 0000000000..ef342e1efc --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanTests.kt @@ -0,0 +1,38 @@ +package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritydslbean + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.util.function.Consumer + + +@ExtendWith(SpringTestContextExtension::class) +class ServerHttpSecurityDslBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var webTest: WebTestClient + + @Test + fun `ServerHttpSecurityDslBean`() { + this.spring.register(ServerHttpSecurityDslBeanConfiguration::class.java).autowire() + + // @formatter:off + this.webTest + .get() + .uri("http://localhost/") + .exchange() + .expectHeader().location("https://localhost/") + .expectHeader().value("Content-Security-Policy", Consumer { csp -> + assertThat(csp).isEqualTo("object-src 'none'") + }) + // @formatter:on + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt new file mode 100644 index 0000000000..77c16a7d07 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt @@ -0,0 +1,43 @@ +package org.springframework.security.kt.docs.reactive.configuration.toplevelcustomizerbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.Customizer +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.configurers.HeadersConfigurer +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.server.SecurityWebFilterChain + +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class TopLevelCustomizerBeanConfiguration { + + @Bean + fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + // @formatter:off + http + .authorizeExchange({ exchanges -> exchanges + .anyExchange().authenticated() + }) + return http.build() + // @formatter:on + } + + // tag::headersCustomizer[] + @Bean + fun headersSecurity(): Customizer { + // @formatter:off + return Customizer { headers -> headers + .contentSecurityPolicy { csp -> csp + // <1> + .policyDirectives("object-src 'none'") + } + } + // @formatter:on + } + // end::headersCustomizer[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt new file mode 100644 index 0000000000..9b81fe2e7a --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt @@ -0,0 +1,35 @@ +package org.springframework.security.kt.docs.reactive.configuration.toplevelcustomizerbean + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.reactive.server.WebTestClient +import java.util.function.Consumer + +@ExtendWith(SpringTestContextExtension::class) +class TopLevelCustomizerBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var webTest: WebTestClient + + @Test + fun `top level dsl bean`() { + this.spring.register(TopLevelCustomizerBeanConfiguration::class.java).autowire() + + // @formatter:off + this.webTest + .get() + .uri("http://localhost/") + .exchange() + .expectHeader().value("Content-Security-Policy", Consumer { csp -> + assertThat(csp).isEqualTo("object-src 'none'") + }) + // @formatter:on + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt new file mode 100644 index 0000000000..e39096ea97 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt @@ -0,0 +1,36 @@ +package org.springframework.security.kt.docs.reactive.configuration.topleveldslbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHeadersDsl +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.web.server.SecurityWebFilterChain + + +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class TopLevelDslBeanConfiguration { + + @Bean + fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + + // tag::headersSecurity[] + @Bean + fun headersSecurity(): ServerHeadersDsl.() -> Unit { + return { + contentSecurityPolicy { + // <1> + policyDirectives = "object-src 'none'" + } + } + } + // end::headersSecurity[] +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanTests.kt new file mode 100644 index 0000000000..ad77bec513 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanTests.kt @@ -0,0 +1,37 @@ +package org.springframework.security.kt.docs.reactive.configuration.topleveldslbean + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.util.function.Consumer + + +@ExtendWith(SpringTestContextExtension::class) +class TopLevelDslBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var webTest: WebTestClient + + @Test + fun `HttpSecurityDslBean`() { + this.spring.register(TopLevelDslBeanConfiguration::class.java).autowire() + + // @formatter:off + this.webTest + .get() + .uri("http://localhost/") + .exchange() + .expectHeader().value("Content-Security-Policy", Consumer { csp -> + assertThat(csp).isEqualTo("object-src 'none'") + }) + // @formatter:on + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt new file mode 100644 index 0000000000..e69be0ab2f --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2004-present 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.kt.docs.servlet.configuration.customizerbeanordering + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.security.config.Customizer +import org.springframework.security.config.ThrowingCustomizer +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.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +internal class CustomizerBeanOrderingConfiguration { + // tag::sample[] + @Bean // <4> + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + // @formatter:off + http + .authorizeHttpRequests({ requests -> requests + .anyRequest().authenticated() + }) + return http.build() + // @formatter:on + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) // <2> + fun userAuthorization(): ThrowingCustomizer { + // @formatter:off + return ThrowingCustomizer { http -> http + .authorizeHttpRequests { requests -> requests + .requestMatchers("/users/**").hasRole("USER") + } + } + // @formatter:on + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // <1> + fun adminAuthorization(): ThrowingCustomizer { + // @formatter:off + return ThrowingCustomizer { http -> http + .authorizeHttpRequests { requests -> requests + .requestMatchers("/admins/**").hasRole("ADMIN") + } + } + // @formatter:on + } + + // <3> + + @Bean + fun contentSecurityPolicy(): Customizer> { + // @formatter:off + return Customizer { headers -> headers + .contentSecurityPolicy { csp -> csp + .policyDirectives("object-src 'none'") + } + } + // @formatter:on + } + + @Bean + fun contentTypeOptions(): Customizer> { + // @formatter:off + return Customizer { headers -> headers + .contentTypeOptions(Customizer.withDefaults()) + } + // @formatter:on + } + + @Bean + fun httpsRedirect(): Customizer> { + // @formatter:off + return Customizer.withDefaults>() + // @formatter:on + } + // end::sample[] +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt new file mode 100644 index 0000000000..9f79be9042 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2004-present 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.kt.docs.servlet.configuration.customizerbeanordering + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers + +/** + * + */ +@ExtendWith(SpringTestContextExtension::class) +class CustomizerBeanOrderingTests { + @JvmField + val spring = SpringTestContext(this).mockMvcAfterSpringSecurityOk() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun authorizationOrdered() { + this.spring.register(CustomizerBeanOrderingConfiguration::class.java).autowire() + // @formatter:off + this.mockMvc.get("https://localhost/admins/1") { + with(user("admin").roles("ADMIN")) + }.andExpect { + status { isOk() } + } + this.mockMvc.get("https://localhost/admins/1") { + with(user("user").roles("USER")) + }.andExpect { + status { isForbidden() } + } + this.mockMvc.get("https://localhost/users/1") { + with(user("user").roles("USER")) + }.andExpect { + status { isOk() } + } + this.mockMvc.get("https://localhost/users/1") { + with(user("noUserRole").roles("OTHER")) + }.andExpect { + status { isForbidden() } + } + this.mockMvc.get("https://localhost/other") { + with(user("authenticated").roles("OTHER")) + }.andExpect { + status { isOk() } + } + // @formatter:on + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt new file mode 100644 index 0000000000..b0f733c879 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2004-present 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.kt.docs.servlet.configuration.dslbeanordering + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.security.config.Customizer +import org.springframework.security.config.ThrowingCustomizer +import org.springframework.security.config.annotation.web.HeadersDsl +import org.springframework.security.config.annotation.web.HttpSecurityDsl +import org.springframework.security.config.annotation.web.HttpsRedirectDsl +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.configurers.HeadersConfigurer +import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +internal class DslBeanOrderingConfiguration { + // tag::sample[] + // All of the Java Modular Configuration is applied first <1> + + @Bean // <5> + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + // @formatter:off + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + // @formatter:on + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) // <3> + fun userAuthorization(): HttpSecurityDsl.() -> Unit { + // @formatter:off + return { + authorizeHttpRequests { + authorize("/users/**", hasRole("USER")) + } + } + // @formatter:on + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // <2> + fun adminAuthorization(): HttpSecurityDsl.() -> Unit { + // @formatter:off + return { + authorizeHttpRequests { + authorize("/admins/**", hasRole("ADMIN")) + } + } + // @formatter:on + } + + // <4> + + @Bean + fun contentSecurityPolicy(): HeadersDsl.() -> Unit { + // @formatter:off + return { + contentSecurityPolicy { + policyDirectives = "object-src 'none'" + } + } + // @formatter:on + } + + @Bean + fun contentTypeOptions(): HeadersDsl.() -> Unit { + // @formatter:off + return { + contentTypeOptions { } + } + // @formatter:on + } + + @Bean + fun httpsRedirect(): HttpsRedirectDsl.() -> Unit { + // @formatter:off + return { } + // @formatter:on + } + // end::sample[] +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingTests.kt new file mode 100644 index 0000000000..f24123c041 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingTests.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2004-present 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.kt.docs.servlet.configuration.dslbeanordering + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.kt.docs.servlet.configuration.customizerbeanordering.CustomizerBeanOrderingConfiguration +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * + */ +@ExtendWith(SpringTestContextExtension::class) +class DslBeanOrderingTests { + @JvmField + val spring = SpringTestContext(this).mockMvcAfterSpringSecurityOk() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun dslOrdered() { + this.spring.register(DslBeanOrderingConfiguration::class.java).autowire() + // @formatter:off + this.mockMvc.get("https://localhost/admins/1") { + with(user("admin").roles("ADMIN")) + }.andExpect { + status { isOk() } + } + this.mockMvc.get("https://localhost/admins/1") { + with(user("user").roles("USER")) + }.andExpect { + status { isForbidden() } + } + this.mockMvc.get("https://localhost/users/1") { + with(user("user").roles("USER")) + }.andExpect { + status { isOk() } + } + this.mockMvc.get("https://localhost/users/1") { + with(user("noUserRole").roles("OTHER")) + }.andExpect { + status { isForbidden() } + } + this.mockMvc.get("https://localhost/other") { + with(user("authenticated").roles("OTHER")) + }.andExpect { + status { isOk() } + } + // @formatter:on + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.kt new file mode 100644 index 0000000000..839ef720d6 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.kt @@ -0,0 +1,46 @@ +package org.springframework.security.kt.docs.servlet.configuration.httpsecuritycustomizerbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.Customizer +import org.springframework.security.config.ThrowingCustomizer +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.configurers.HeadersConfigurer +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain + +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class HttpSecurityCustomizerBeanConfiguration { + + @Bean + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + + + // tag::httpSecurityCustomizer[] + @Bean + fun httpSecurityCustomizer(): ThrowingCustomizer { + // @formatter:off + return ThrowingCustomizer { http -> http + .headers { headers -> headers + .contentSecurityPolicy { csp -> csp + // <1> + .policyDirectives("object-src 'none'") + } + } + // <2> + .redirectToHttps(Customizer.withDefaults()) + } + // @formatter:on + } + // end::httpSecurityCustomizer[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.kt new file mode 100644 index 0000000000..d994e8b1e2 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.kt @@ -0,0 +1,35 @@ +package org.springframework.security.kt.docs.servlet.configuration.httpsecuritycustomizerbean + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +@ExtendWith(SpringTestContextExtension::class) +class HttpSecurityCustomizerBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `httpsecurity customizer config`() { + this.spring.register(HttpSecurityCustomizerBeanConfiguration::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + redirectedUrl("https://localhost/") + } + this.mockMvc.get("https://localhost/") + .andExpect { + header { + string("Content-Security-Policy", "object-src 'none'") + } + } + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanConfiguration.kt new file mode 100644 index 0000000000..d627999705 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanConfiguration.kt @@ -0,0 +1,52 @@ +package org.springframework.security.kt.docs.servlet.configuration.httpsecuritydslbean + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.HeadersDsl +import org.springframework.security.config.annotation.web.HttpSecurityDsl +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.invoke +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.web.SecurityFilterChain +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.servlet.config.annotation.EnableWebMvc + + +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class HttpSecurityDslBeanConfiguration { + + @Bean + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + + // tag::httpSecurityDslBean[] + @Bean + fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit { + return { + headers { + contentSecurityPolicy { + // <1> + policyDirectives = "object-src 'none'" + } + } + // <2> + redirectToHttps { } + } + } + // end::httpSecurityDslBean[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanTests.kt new file mode 100644 index 0000000000..ad3b76e517 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanTests.kt @@ -0,0 +1,36 @@ +package org.springframework.security.kt.docs.servlet.configuration.httpsecuritydslbean + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + + +@ExtendWith(SpringTestContextExtension::class) +class HttpSecurityDslBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `HttpSecurityDslBean`() { + this.spring.register(HttpSecurityDslBeanConfiguration::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + redirectedUrl("https://localhost/") + } + + this.mockMvc.get("https://localhost/") + .andExpect { + header { + string("Content-Security-Policy", "object-src 'none'") + } + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt new file mode 100644 index 0000000000..820c816b28 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt @@ -0,0 +1,42 @@ +package org.springframework.security.kt.docs.servlet.configuration.toplevelcustomizerbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.Customizer +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.configurers.HeadersConfigurer +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain + +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class TopLevelCustomizerBeanConfiguration { + + @Bean + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + // @formatter:off + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + // @formatter:on + } + + // tag::headersCustomizer[] + @Bean + fun headersSecurity(): Customizer> { + // @formatter:off + return Customizer { headers -> headers + .contentSecurityPolicy { csp -> csp + // <1> + .policyDirectives("object-src 'none'") + } + } + // @formatter:on + } + // end::headersCustomizer[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt new file mode 100644 index 0000000000..20b7c14016 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt @@ -0,0 +1,31 @@ +package org.springframework.security.kt.docs.servlet.configuration.toplevelcustomizerbean + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +@ExtendWith(SpringTestContextExtension::class) +class TopLevelCustomizerBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `top level dsl bean`() { + this.spring.register(TopLevelCustomizerBeanConfiguration::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { + string("Content-Security-Policy", "object-src 'none'") + } + } + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt new file mode 100644 index 0000000000..7e786bae50 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt @@ -0,0 +1,40 @@ +package org.springframework.security.kt.docs.servlet.configuration.topleveldslbean + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.HeadersDsl +import org.springframework.security.config.annotation.web.HttpSecurityDsl +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.invoke +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.servlet.config.annotation.EnableWebMvc + + +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class TopLevelDslBeanConfiguration { + + @Bean + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + + // tag::headersSecurity[] + @Bean + fun headersSecurity(): HeadersDsl.() -> Unit { + return { + contentSecurityPolicy { + // <1> + policyDirectives = "object-src 'none'" + } + } + } + // end::headersSecurity[] +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanTests.kt new file mode 100644 index 0000000000..92e8e0ccde --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanTests.kt @@ -0,0 +1,31 @@ +package org.springframework.security.kt.docs.servlet.configuration.topleveldslbean + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + + +@ExtendWith(SpringTestContextExtension::class) +class TopLevelDslBeanTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `HttpSecurityDslBean`() { + this.spring.register(TopLevelDslBeanConfiguration::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { + string("Content-Security-Policy", "object-src 'none'") + } + } + } +}