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 b856daf200..9e979544b0 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 @@ -28,6 +28,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.annotation.security.DenyAll; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -40,9 +44,12 @@ import org.springframework.aop.Advisor; import org.springframework.aop.config.AopConfigUtils; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; +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.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.Bean; @@ -1018,6 +1025,43 @@ public class PrePostMethodSecurityConfigurationTests { assertThat(service.getIdPath("uid")).isEqualTo("uid"); } + @Test + @WithMockUser + public void prePostMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.preAuthorizePermitAll(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void securedMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.securedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::secured); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void jsr250MethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.jsr250RolesAllowedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(this.methodSecurityService::jsr250RolesAllowed); + verify(handler).onError(any()); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -1655,4 +1699,47 @@ public class PrePostMethodSecurityConfigurationTests { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> 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; + } + + } + } 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 cf845ebb61..295ca2c7ba 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 @@ -23,12 +23,17 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; @@ -62,6 +67,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; /** @@ -235,6 +241,25 @@ public class ReactiveMethodSecurityConfigurationTests { verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class)); } + @Test + public void prePostMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.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); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + StepVerifier + .create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectError() + .verify(); + verify(handler).onError(any()); + } + private static Consumer authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } @@ -388,4 +413,30 @@ public class ReactiveMethodSecurityConfigurationTests { } + @Configuration + @EnableReactiveMethodSecurity + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + PrePostMethodSecurityConfigurationTests.ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new PrePostMethodSecurityConfigurationTests.ObservationRegistryPostProcessor(handler); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java index a3fee08102..05e94d8a30 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java @@ -48,6 +48,12 @@ import org.springframework.util.StringUtils; @ReactiveMethodSecurityService.Mask("classmask") public interface ReactiveMethodSecurityService { + @PreAuthorize("hasRole('USER')") + Mono preAuthorizeUser(); + + @PreAuthorize("hasRole('ADMIN')") + Mono preAuthorizeAdmin(); + @PreAuthorize("hasRole('ADMIN')") @HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class) Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java index 7b8a893d17..590184f67f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java @@ -25,6 +25,16 @@ import org.springframework.security.authorization.AuthorizationDeniedException; public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService { + @Override + public Mono preAuthorizeUser() { + return Mono.just("user"); + } + + @Override + public Mono preAuthorizeAdmin() { + return Mono.just("admin"); + } + @Override public Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber) { return Mono.just(cardNumber); 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 0aad4d777c..3f3059fc28 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 @@ -18,12 +18,20 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; @@ -33,6 +41,7 @@ import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationObservationContext; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -43,6 +52,7 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -63,8 +73,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -153,7 +165,8 @@ public class AuthorizeHttpRequestsConfigurerTests { @Test public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = this.spring.getContext() + .getBean(ObjectPostProcessorConfig.class).objectPostProcessor; verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class)); verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class)); } @@ -623,6 +636,20 @@ public class AuthorizeHttpRequestsConfigurerTests { this.mvc.perform(requestWithUser).andExpect(status().isOk()); } + @Test + public void getWhenObservationRegistryThenObserves() throws Exception { + this.spring.register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class).autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk()); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthorizationObservationContext); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthorizationObservationContext); + this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden()); + verify(handler).onError(any()); + } + @Configuration @EnableWebSecurity static class GrantedAuthorityDefaultHasRoleConfig { @@ -1015,6 +1042,12 @@ public class AuthorizeHttpRequestsConfigurerTests { // @formatter:on } + @Bean + UserDetailsService users() { + return new InMemoryUserDetailsManager( + User.withUsername("user").password("{noop}password").roles("USER").build()); + } + } @Configuration @@ -1212,4 +1245,47 @@ public class AuthorizeHttpRequestsConfigurerTests { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> 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; + } + + } + } 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 25cfac15e6..d1cc2859bd 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 @@ -16,14 +16,23 @@ package org.springframework.security.config.annotation.web.configurers; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; 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.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationObservationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -50,7 +59,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; 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.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -161,6 +172,23 @@ public class HttpBasicConfigurerTests { .saveContext(any(SecurityContext.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); } + @Test + public void httpBasicWhenObservationRegistryThenObserves() throws Exception { + this.spring.register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.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()).anyMatch((c) -> c instanceof AuthenticationObservationContext); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthenticationObservationContext); + this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized()); + verify(handler).onError(context.capture()); + assertThat(context.getValue()).isInstanceOf(AuthenticationObservationContext.class); + } + @Configuration @EnableWebSecurity static class ObjectPostProcessorConfig { @@ -384,4 +412,47 @@ public class HttpBasicConfigurerTests { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> 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; + } + + } + } 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 97acdeaf05..dedd903f38 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 @@ -26,13 +26,20 @@ import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -95,7 +102,9 @@ import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSo import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; +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.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken; @@ -381,6 +390,30 @@ public class WebSocketMessageBrokerSecurityConfigurationTests { clientInboundChannel().send(message("/anonymous")); } + @Test + public void sendMessageWhenObservationRegistryThenObserves() { + loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.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); + verify(observationHandler).onStart(any()); + verify(observationHandler).onStop(any()); + 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 + } + verify(observationHandler).onError(any()); + } + private void assertHandshake(HttpServletRequest request) { TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token); @@ -892,4 +925,47 @@ public class WebSocketMessageBrokerSecurityConfigurationTests { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> 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; + } + + } + }