diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java new file mode 100644 index 0000000000..2bf71a4401 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class MethodObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index 103ea5c8f6..47d5d23f76 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -68,8 +68,7 @@ final class MethodSecuritySelector implements ImportSelector { imports.add(AuthorizationProxyDataConfiguration.class.getName()); } if (isObservabilityPresent) { - imports.add( - "org.springframework.security.config.annotation.observation.configuration.ObservationConfiguration"); + imports.add(MethodObservationConfiguration.class.getName()); } return imports.toArray(new String[0]); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java new file mode 100644 index 0000000000..0778df14d2 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveMethodObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java index 7d1d241f16..41f356772f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.Role; import org.springframework.core.type.AnnotationMetadata; @@ -82,6 +83,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Fallback static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( ReactiveMethodSecurityConfiguration configuration) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java index aa99988e27..c204b33aaa 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java @@ -62,8 +62,7 @@ class ReactiveMethodSecuritySelector implements ImportSelector { imports.add(AuthorizationProxyDataConfiguration.class.getName()); } if (isObservabilityPresent) { - imports.add( - "org.springframework.security.config.annotation.observation.configuration.ReactiveObservationConfiguration"); + imports.add(ReactiveMethodObservationConfiguration.class.getName()); } imports.add(AuthorizationProxyConfiguration.class.getName()); return imports.toArray(new String[0]); diff --git a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/AbstractObservationObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/AbstractObservationObjectPostProcessor.java deleted file mode 100644 index a9b58a98c3..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/AbstractObservationObjectPostProcessor.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.observation.configuration; - -import java.util.function.BiFunction; -import java.util.function.Function; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; - -abstract class AbstractObservationObjectPostProcessor implements ObjectPostProcessor { - - private final ObjectProvider observationRegistry; - - private final BiFunction wrapper; - - AbstractObservationObjectPostProcessor(ObjectProvider observationRegistry, - Function constructor) { - this(observationRegistry, (registry, object) -> constructor.apply(registry)); - } - - AbstractObservationObjectPostProcessor(ObjectProvider observationRegistry, - BiFunction constructor) { - this.observationRegistry = observationRegistry; - this.wrapper = constructor; - } - - @Override - public O1 postProcess(O1 object) { - ObservationRegistry registry = this.observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP); - if (registry.isNoop()) { - return object; - } - return (O1) this.wrapper.apply(registry, object); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/ObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/ObservationConfiguration.java deleted file mode 100644 index 269f963cfa..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/ObservationConfiguration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.observation.configuration; - -import io.micrometer.observation.ObservationRegistry; -import jakarta.servlet.http.HttpServletRequest; -import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.ObservationAuthenticationManager; -import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; -import org.springframework.security.authorization.method.MethodInvocationResult; -import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.web.FilterChainProxy; -import org.springframework.security.web.ObservationFilterChainDecorator; - -@Configuration(proxyBeanMethods = false) -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -class ObservationConfiguration { - - private final ObjectProvider observationRegistry; - - ObservationConfiguration(ObjectProvider observationRegistry) { - this.observationRegistry = observationRegistry; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor> methodAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationAuthorizationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationAuthorizationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor> webAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationAuthorizationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor authenticationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationAuthenticationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor filterChainDecoratorPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationFilterChainDecorator::new) { - }; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/ReactiveObservationConfiguration.java deleted file mode 100644 index f898e77d8d..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/ReactiveObservationConfiguration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.observation.configuration; - -import io.micrometer.observation.ObservationRegistry; -import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; -import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; -import org.springframework.security.authorization.ReactiveAuthorizationManager; -import org.springframework.security.authorization.method.MethodInvocationResult; -import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.web.server.ObservationWebFilterChainDecorator; -import org.springframework.security.web.server.WebFilterChainProxy; -import org.springframework.web.server.ServerWebExchange; - -@Configuration(proxyBeanMethods = false) -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -class ReactiveObservationConfiguration { - - private final ObjectProvider observationRegistry; - - ReactiveObservationConfiguration(ObjectProvider observationRegistry) { - this.observationRegistry = observationRegistry; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor> methodAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationReactiveAuthorizationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationReactiveAuthorizationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor> webAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationReactiveAuthorizationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor authenticationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationReactiveAuthenticationManager::new) { - }; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor filterChainDecoratorPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationWebFilterChainDecorator::new) { - }; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java new file mode 100644 index 0000000000..b1585151f7 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.rsocket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.server.ObservationWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; +import org.springframework.web.server.ServerWebExchange; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthenticationManager postProcess(ReactiveAuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationReactiveAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public WebFilterChainDecorator postProcess(WebFilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationWebFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java index 9c354596ae..6e18bc4396 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java @@ -44,8 +44,7 @@ class ReactiveObservationImportSelector implements ImportSelector { if (!observabilityPresent) { return new String[0]; } - return new String[] { - "org.springframework.security.config.annotation.observation.configuration.ReactiveObservationConfiguration" }; + return new String[] { ReactiveObservationConfiguration.class.getName() }; } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java new file mode 100644 index 0000000000..29a2b2c119 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configuration; + +import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ObservationAuthenticationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.FilterChainProxy.FilterChainDecorator; +import org.springframework.security.web.ObservationFilterChainDecorator; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthenticationManager postProcess(AuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public FilterChainDecorator postProcess(FilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java index 29bdd65d9a..202d150f2f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java @@ -43,8 +43,7 @@ class ObservationImportSelector implements ImportSelector { if (!observabilityPresent) { return new String[0]; } - return new String[] { - "org.springframework.security.config.annotation.observation.configuration.ObservationConfiguration" }; + return new String[] { ObservationConfiguration.class.getName() }; } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java new file mode 100644 index 0000000000..f8487811b2 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.server.ObservationWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; +import org.springframework.web.server.ServerWebExchange; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthenticationManager postProcess(ReactiveAuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationReactiveAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public WebFilterChainDecorator postProcess(WebFilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationWebFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java index fcdee93626..5b4bdaebf0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java @@ -43,8 +43,7 @@ class ReactiveObservationImportSelector implements ImportSelector { if (!observabilityPresent) { return new String[0]; } - return new String[] { - "org.springframework.security.config.annotation.observation.configuration.ReactiveObservationConfiguration" }; + return new String[] { ReactiveObservationConfiguration.class.getName() }; } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/WebSocketObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java similarity index 59% rename from config/src/main/java/org/springframework/security/config/annotation/observation/configuration/WebSocketObservationConfiguration.java rename to config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java index 2877a0b913..3affe0558c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/observation/configuration/WebSocketObservationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.config.annotation.observation.configuration; +package org.springframework.security.config.annotation.web.socket; import io.micrometer.observation.ObservationRegistry; @@ -27,22 +27,29 @@ import org.springframework.messaging.Message; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; @Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) class WebSocketObservationConfiguration { - private final ObjectProvider observationRegistry; - - WebSocketObservationConfiguration(ObjectProvider observationRegistry) { - this.observationRegistry = observationRegistry; - } + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - ObjectPostProcessor>> messageAuthorizationManagerPostProcessor() { - return new AbstractObservationObjectPostProcessor<>(this.observationRegistry, - ObservationAuthorizationManager::new) { + static ObjectPostProcessor>> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } }; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java index a8923b6dc0..3eb4e0b445 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java @@ -43,8 +43,7 @@ class WebSocketObservationImportSelector implements ImportSelector { if (!observabilityPresent) { return new String[0]; } - return new String[] { - "org.springframework.security.config.annotation.observation.configuration.WebSocketObservationConfiguration" }; + return new String[] { WebSocketObservationConfiguration.class.getName() }; } } diff --git a/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java b/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java new file mode 100644 index 0000000000..d37d421870 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2024 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.observation; + +import io.micrometer.observation.ObservationPredicate; + +/** + * An {@link ObservationPredicate} that can be used to change which Spring Security + * observations are made with Micrometer. + * + *

+ * By default, web requests are not observed and authentications and authorizations are + * observed. + * + * @author Josh Cummings + * @since 6.4 + */ +public final class SecurityObservationSettings { + + private final boolean observeRequests; + + private final boolean observeAuthentications; + + private final boolean observeAuthorizations; + + private SecurityObservationSettings(boolean observeRequests, boolean observeAuthentications, + boolean observeAuthorizations) { + this.observeRequests = observeRequests; + this.observeAuthentications = observeAuthentications; + this.observeAuthorizations = observeAuthorizations; + } + + /** + * Make no Spring Security observations + * @return a {@link SecurityObservationSettings} with all exclusions turned on + */ + public static SecurityObservationSettings noObservations() { + return new SecurityObservationSettings(false, false, false); + } + + /** + * Begin the configuration of a {@link SecurityObservationSettings} + * @return a {@link Builder} where filter chain observations are off and authn/authz + * observations are on + */ + public static Builder withDefaults() { + return new Builder(false, true, true); + } + + public boolean shouldObserveRequests() { + return this.observeRequests; + } + + public boolean shouldObserveAuthentications() { + return this.observeAuthentications; + } + + public boolean shouldObserveAuthorizations() { + return this.observeAuthorizations; + } + + /** + * A builder for configuring a {@link SecurityObservationSettings} + */ + public static final class Builder { + + private boolean observeRequests; + + private boolean observeAuthentications; + + private boolean observeAuthorizations; + + Builder(boolean observeRequests, boolean observeAuthentications, boolean observeAuthorizations) { + this.observeRequests = observeRequests; + this.observeAuthentications = observeAuthentications; + this.observeAuthorizations = observeAuthorizations; + } + + public Builder shouldObserveRequests(boolean excludeFilters) { + this.observeRequests = excludeFilters; + return this; + } + + public Builder shouldObserveAuthentications(boolean excludeAuthentications) { + this.observeAuthentications = excludeAuthentications; + return this; + } + + public Builder shouldObserveAuthorizations(boolean excludeAuthorizations) { + this.observeAuthorizations = excludeAuthorizations; + return this; + } + + public SecurityObservationSettings build() { + return new SecurityObservationSettings(this.observeRequests, this.observeAuthentications, + this.observeAuthorizations); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 9e979544b0..73e65c78a7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -87,6 +87,7 @@ import org.springframework.security.authorization.method.PrePostTemplateDefaults import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestParentApplicationContextExecutionListener; @@ -114,6 +115,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link PrePostMethodSecurityConfiguration}. @@ -1062,6 +1064,43 @@ public class PrePostMethodSecurityConfigurationTests { verify(handler).onError(any()); } + @Test + @WithMockUser + public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.preAuthorizePermitAll(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize); + verifyNoInteractions(handler); + } + + @Test + @WithMockUser + public void securedMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.securedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verifyNoInteractions(handler); + } + + @Test + @WithMockUser + public void jsr250MethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.jsr250RolesAllowedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verifyNoInteractions(handler); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -1742,4 +1781,14 @@ public class PrePostMethodSecurityConfigurationTests { } + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java index 295ca2c7ba..042ed87c7e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java @@ -33,9 +33,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; @@ -55,6 +57,7 @@ import org.springframework.security.authorization.method.AuthorizeReturnObject; import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; import org.springframework.security.config.Customizer; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; @@ -69,6 +72,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Tadaya Tsuyukubo @@ -260,6 +264,27 @@ public class ReactiveMethodSecurityConfigurationTests { verify(handler).onError(any()); } + @Test + @WithMockUser + public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + Authentication user = TestAuthentication.authenticatedUser(); + StepVerifier + .create(service.preAuthorizeUser().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectNextCount(1) + .verifyComplete(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + StepVerifier + .create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectError() + .verify(); + verifyNoInteractions(handler); + } + private static Consumer authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } @@ -432,9 +457,37 @@ public class ReactiveMethodSecurityConfigurationTests { } @Bean - PrePostMethodSecurityConfigurationTests.ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObservationRegistryPostProcessor observationRegistryPostProcessor( ObjectProvider> handler) { - return new PrePostMethodSecurityConfigurationTests.ObservationRegistryPostProcessor(handler); + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 3f3059fc28..db9976f087 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -47,6 +47,7 @@ import org.springframework.security.config.annotation.web.AbstractRequestMatcher import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; @@ -80,6 +81,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -650,6 +652,18 @@ public class AuthorizeHttpRequestsConfigurerTests { verify(handler).onError(any()); } + @Test + public void getWhenExcludeAuthorizationObservationsThenUnobserved() throws Exception { + this.spring + .register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk()); + this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden()); + verifyNoInteractions(handler); + } + @Configuration @EnableWebSecurity static class GrantedAuthorityDefaultHasRoleConfig { @@ -1288,4 +1302,14 @@ public class AuthorizeHttpRequestsConfigurerTests { } + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java index d1cc2859bd..bbb78923d3 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java @@ -39,6 +39,7 @@ import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.AuthenticationException; @@ -63,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.springframework.security.config.Customizer.withDefaults; @@ -189,6 +191,26 @@ public class HttpBasicConfigurerTests { assertThat(context.getValue()).isInstanceOf(AuthenticationObservationContext.class); } + @Test + public void httpBasicWhenExcludeAuthenticationObservationsThenUnobserved() throws Exception { + this.spring + .register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext); + context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext); + this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized()); + verify(handler, never()).onError(any()); + } + @Configuration @EnableWebSecurity static class ObjectPostProcessorConfig { @@ -455,4 +477,14 @@ public class HttpBasicConfigurerTests { } + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthentications(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java index dedd903f38..099207d0b5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java @@ -69,6 +69,7 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -106,6 +107,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken; public class WebSocketMessageBrokerSecurityConfigurationTests { @@ -414,6 +416,28 @@ public class WebSocketMessageBrokerSecurityConfigurationTests { verify(observationHandler).onError(any()); } + @Test + public void sendMessageWhenExcludeAuthorizationObservationsThenUnobserved() { + loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.class, SelectableObservationsConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + Message message = message(headers, "/authenticated"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + clientInboundChannel().send(message); + ObservationHandler observationHandler = this.context.getBean(ObservationHandler.class); + headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + message = message(headers, "/denyAll"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + try { + clientInboundChannel().send(message); + } + catch (MessageDeliveryException ex) { + // okay + } + verifyNoInteractions(observationHandler); + } + private void assertHandshake(HttpServletRequest request) { TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token); @@ -968,4 +992,14 @@ public class WebSocketMessageBrokerSecurityConfigurationTests { } + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java b/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java new file mode 100644 index 0000000000..75dd6c2877 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2024 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.observation; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SecurityObservationSettings} + */ +public class SecurityObservationSettingsTests { + + @Test + void withDefaultsThenFilterOffAuthenticationOnAuthorizationOn() { + SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults().build(); + assertThat(defaults.shouldObserveRequests()).isFalse(); + assertThat(defaults.shouldObserveAuthentications()).isTrue(); + assertThat(defaults.shouldObserveAuthorizations()).isTrue(); + } + + @Test + void noObservationsWhenConstructedThenAllOff() { + SecurityObservationSettings defaults = SecurityObservationSettings.noObservations(); + assertThat(defaults.shouldObserveRequests()).isFalse(); + assertThat(defaults.shouldObserveAuthentications()).isFalse(); + assertThat(defaults.shouldObserveAuthorizations()).isFalse(); + } + + @Test + void withDefaultsWhenExclusionsThenInstanceReflects() { + SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults() + .shouldObserveAuthentications(false) + .shouldObserveAuthorizations(false) + .shouldObserveRequests(true) + .build(); + assertThat(defaults.shouldObserveRequests()).isTrue(); + assertThat(defaults.shouldObserveAuthentications()).isFalse(); + assertThat(defaults.shouldObserveAuthorizations()).isFalse(); + } + +} diff --git a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc index 0534fae582..2b0db4865e 100644 --- a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc +++ b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc @@ -187,7 +187,7 @@ Xml:: If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`. However, this may turn off observations for more than just Spring Security. -Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following: +Instead, you can publish a `SecurityObservationSettings` like the following: [tabs] ====== @@ -196,9 +196,8 @@ Java:: [source,java,role="primary"] ---- @Bean -ObservationRegistryCustomizer noSpringSecurityObservations() { - ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security."); - return (registry) -> registry.observationConfig().observationPredicate(predicate); +SecurityObservationSettings noSpringSecurityObservations() { + return SecurityObservationSettings.noObservations(); } ---- @@ -207,17 +206,77 @@ Kotlin:: [source,kotlin,role="secondary"] ---- @Bean -fun noSpringSecurityObservations(): ObservationRegistryCustomizer { - ObservationPredicate predicate = (name: String, context: Observation.Context) -> !name.startsWith("spring.security.") - (registry: ObservationRegistry) -> registry.observationConfig().observationPredicate(predicate) +fun noSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.noObservations() } ---- ====== +and then Spring Security will not wrap any filter chains, authentications, or authorizations in their `ObservationXXX` counterparts. + [TIP] There is no facility for disabling observations with XML support. Instead, simply do not set the `observation-registry-ref` attribute. +You can also disable security for only a subset of Security's observations. +For example, the `SecurityObservationSettings` bean excludes the filter chain observations by default. +So, you can also do: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings defaultSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults().build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun defaultSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.withDefaults().build() +} +---- +====== + +Or you can turn on and off observations individually, based on the defaults: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings allSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults() + .shouldObserveFilterChains(true).build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun allSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservabilityDefaults.builder() + .shouldObserveFilterChains(true).build() +} +---- +====== + +[NOTE] +===== +For backward compatibility, all Spring Security observations are made unless a `SecurityObservationSettings` is published. +===== + [[webflux-observability-tracing-listing]] === Trace Listing diff --git a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc index d571c37ff6..e9d5f33090 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc @@ -192,7 +192,7 @@ Xml:: If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`. However, this may turn off observations for more than just Spring Security. -Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following: +Instead, you can publish a `SecurityObservationSettings` like the following: [tabs] ====== @@ -201,9 +201,8 @@ Java:: [source,java,role="primary"] ---- @Bean -ObservationRegistryCustomizer noSpringSecurityObservations() { - ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security."); - return (registry) -> registry.observationConfig().observationPredicate(predicate); +SecurityObservationSettings noSpringSecurityObservations() { + return SecurityObservationSettings.noObservations(); } ---- @@ -212,21 +211,77 @@ Kotlin:: [source,kotlin,role="secondary"] ---- @Bean -fun noSpringSecurityObservations(): ObservationRegistryCustomizer { - val predicate = ObservationPredicate { name: String, _: Observation.Context? -> - !name.startsWith("spring.security.") - } - return ObservationRegistryCustomizer { registry: ObservationRegistry -> - registry.observationConfig().observationPredicate(predicate) - } +fun noSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.noObservations() } ---- ====== +and then Spring Security will not wrap any filter chains, authentications, or authorizations in their `ObservationXXX` counterparts. + [TIP] There is no facility for disabling observations with XML support. Instead, simply do not set the `observation-registry-ref` attribute. +You can also disable security for only a subset of Security's observations. +For example, the `SecurityObservationSettings` bean excludes the filter chain observations by default. +So, you can also do: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings defaultSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults().build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun defaultSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.withDefaults().build() +} +---- +====== + +Or you can turn on and off observations individually, based on the defaults: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings allSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults() + .shouldObserveFilterChains(true).build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun allSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.builder() + .shouldObserveFilterChains(true).build() +} +---- +====== + +[NOTE] +===== +For backward compatibility, the all Spring Security observations are made unless a `SecurityObservationSettings` is published. +===== + [[observability-tracing-listing]] === Trace Listing