diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java index 95a5841765..a1210652ee 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java @@ -16,12 +16,17 @@ package org.springframework.security.config.annotation.method.configuration; +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + import org.springframework.aop.Advisor; -import org.springframework.beans.factory.annotation.Autowired; +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.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.Jsr250AuthorizationManager; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -40,28 +45,28 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy; @Role(BeanDefinition.ROLE_INFRASTRUCTURE) final class Jsr250MethodSecurityConfiguration { - private final Jsr250AuthorizationManager jsr250AuthorizationManager = new Jsr250AuthorizationManager(); - - private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder - .getContextHolderStrategy(); - @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor jsr250AuthorizationMethodInterceptor() { + static Advisor jsr250AuthorizationMethodInterceptor(ObjectProvider defaultsProvider, + ObjectProvider strategyProvider, + ObjectProvider registryProvider) { + Jsr250AuthorizationManager jsr250 = new Jsr250AuthorizationManager(); + defaultsProvider.ifAvailable((d) -> jsr250.setRolePrefix(d.getRolePrefix())); + SecurityContextHolderStrategy strategy = strategyProvider + .getIfAvailable(SecurityContextHolder::getContextHolderStrategy); + ObservationRegistry registry = registryProvider.getIfAvailable(() -> ObservationRegistry.NOOP); + AuthorizationManager manager = manager(jsr250, registry); AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor - .jsr250(this.jsr250AuthorizationManager); - interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + .jsr250(manager); + interceptor.setSecurityContextHolderStrategy(strategy); return interceptor; } - @Autowired(required = false) - void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) { - this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } - - @Autowired(required = false) - void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { - this.securityContextHolderStrategy = securityContextHolderStrategy; + static AuthorizationManager manager(AuthorizationManager jsr250, ObservationRegistry registry) { + if (registry.isNoop()) { + return jsr250; + } + return new ObservationAuthorizationManager<>(registry, jsr250); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 1c2a48be72..79a5c54fa7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -16,8 +16,10 @@ package org.springframework.security.config.annotation.method.configuration; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.aop.Advisor; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -26,7 +28,8 @@ import org.springframework.context.annotation.Role; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.authorization.AuthorizationEventPublisher; -import org.springframework.security.authorization.SpringAuthorizationEventPublisher; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; @@ -48,85 +51,80 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy; @Role(BeanDefinition.ROLE_INFRASTRUCTURE) final class PrePostMethodSecurityConfiguration { - private final PreFilterAuthorizationMethodInterceptor preFilterAuthorizationMethodInterceptor = new PreFilterAuthorizationMethodInterceptor(); - - private final AuthorizationManagerBeforeMethodInterceptor preAuthorizeAuthorizationMethodInterceptor; - - private final PreAuthorizeAuthorizationManager preAuthorizeAuthorizationManager = new PreAuthorizeAuthorizationManager(); - - private final AuthorizationManagerAfterMethodInterceptor postAuthorizeAuthorizaitonMethodInterceptor; - - private final PostAuthorizeAuthorizationManager postAuthorizeAuthorizationManager = new PostAuthorizeAuthorizationManager(); - - private final PostFilterAuthorizationMethodInterceptor postFilterAuthorizationMethodInterceptor = new PostFilterAuthorizationMethodInterceptor(); - - private final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - - @Autowired - PrePostMethodSecurityConfiguration(ApplicationContext context) { - this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); - this.preAuthorizeAuthorizationMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(this.preAuthorizeAuthorizationManager); - this.postAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); - this.postAuthorizeAuthorizaitonMethodInterceptor = AuthorizationManagerAfterMethodInterceptor - .postAuthorize(this.postAuthorizeAuthorizationManager); - this.preFilterAuthorizationMethodInterceptor.setExpressionHandler(this.expressionHandler); - this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(this.expressionHandler); - this.expressionHandler.setApplicationContext(context); - AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(context); - this.preAuthorizeAuthorizationMethodInterceptor.setAuthorizationEventPublisher(publisher); - this.postAuthorizeAuthorizaitonMethodInterceptor.setAuthorizationEventPublisher(publisher); + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static Advisor preFilterAuthorizationMethodInterceptor(ObjectProvider defaultsProvider, + ObjectProvider expressionHandlerProvider, + ObjectProvider strategyProvider, ApplicationContext context) { + PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor(); + strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy); + preFilter.setExpressionHandler( + expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context))); + return preFilter; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor preFilterAuthorizationMethodInterceptor() { - return this.preFilterAuthorizationMethodInterceptor; + static Advisor preAuthorizeAuthorizationMethodInterceptor(ObjectProvider defaultsProvider, + ObjectProvider expressionHandlerProvider, + ObjectProvider strategyProvider, + ObjectProvider eventPublisherProvider, + ObjectProvider registryProvider, ApplicationContext context) { + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + manager.setExpressionHandler( + expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context))); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(manager(manager, registryProvider)); + strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy); + eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher); + return preAuthorize; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor preAuthorizeAuthorizationMethodInterceptor() { - return this.preAuthorizeAuthorizationMethodInterceptor; + static Advisor postAuthorizeAuthorizationMethodInterceptor( + ObjectProvider defaultsProvider, + ObjectProvider expressionHandlerProvider, + ObjectProvider strategyProvider, + ObjectProvider eventPublisherProvider, + ObjectProvider registryProvider, ApplicationContext context) { + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + manager.setExpressionHandler( + expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context))); + AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor + .postAuthorize(manager(manager, registryProvider)); + strategyProvider.ifAvailable(postAuthorize::setSecurityContextHolderStrategy); + eventPublisherProvider.ifAvailable(postAuthorize::setAuthorizationEventPublisher); + return postAuthorize; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor postAuthorizeAuthorizationMethodInterceptor() { - return this.postAuthorizeAuthorizaitonMethodInterceptor; + static Advisor postFilterAuthorizationMethodInterceptor(ObjectProvider defaultsProvider, + ObjectProvider expressionHandlerProvider, + ObjectProvider strategyProvider, ApplicationContext context) { + PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor(); + strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy); + postFilter.setExpressionHandler( + expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context))); + return postFilter; } - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor postFilterAuthorizationMethodInterceptor() { - return this.postFilterAuthorizationMethodInterceptor; + private static MethodSecurityExpressionHandler defaultExpressionHandler( + ObjectProvider defaultsProvider, ApplicationContext context) { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + defaultsProvider.ifAvailable((d) -> handler.setDefaultRolePrefix(d.getRolePrefix())); + handler.setApplicationContext(context); + return handler; } - @Autowired(required = false) - void setMethodSecurityExpressionHandler(MethodSecurityExpressionHandler methodSecurityExpressionHandler) { - this.preFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler); - this.preAuthorizeAuthorizationManager.setExpressionHandler(methodSecurityExpressionHandler); - this.postAuthorizeAuthorizationManager.setExpressionHandler(methodSecurityExpressionHandler); - this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler); - } - - @Autowired(required = false) - void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) { - this.preFilterAuthorizationMethodInterceptor.setSecurityContextHolderStrategy(strategy); - this.preAuthorizeAuthorizationMethodInterceptor.setSecurityContextHolderStrategy(strategy); - this.postAuthorizeAuthorizaitonMethodInterceptor.setSecurityContextHolderStrategy(strategy); - this.postFilterAuthorizationMethodInterceptor.setSecurityContextHolderStrategy(strategy); - } - - @Autowired(required = false) - void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) { - this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } - - @Autowired(required = false) - void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { - this.preAuthorizeAuthorizationMethodInterceptor.setAuthorizationEventPublisher(eventPublisher); - this.postAuthorizeAuthorizaitonMethodInterceptor.setAuthorizationEventPublisher(eventPublisher); + static AuthorizationManager manager(AuthorizationManager delegate, + ObjectProvider registryProvider) { + ObservationRegistry registry = registryProvider.getIfAvailable(() -> ObservationRegistry.NOOP); + if (registry.isNoop()) { + return delegate; + } + return new ObservationAuthorizationManager<>(registry, delegate); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index b2ed2ee22d..40e498e512 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -16,6 +16,10 @@ 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.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; @@ -24,8 +28,11 @@ import org.springframework.context.annotation.Role; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.PostAuthorizeReactiveAuthorizationManager; import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager; @@ -43,39 +50,39 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor( + static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor( MethodSecurityExpressionHandler expressionHandler) { return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor( - MethodSecurityExpressionHandler expressionHandler) { - PreAuthorizeReactiveAuthorizationManager authorizationManager = new PreAuthorizeReactiveAuthorizationManager( - expressionHandler); + static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor( + MethodSecurityExpressionHandler expressionHandler, ObjectProvider registryProvider) { + ReactiveAuthorizationManager authorizationManager = manager( + new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider); return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor( + static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor( MethodSecurityExpressionHandler expressionHandler) { return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor( - MethodSecurityExpressionHandler expressionHandler) { - PostAuthorizeReactiveAuthorizationManager authorizationManager = new PostAuthorizeReactiveAuthorizationManager( - expressionHandler); + static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor( + MethodSecurityExpressionHandler expressionHandler, ObjectProvider registryProvider) { + ReactiveAuthorizationManager authorizationManager = manager( + new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider); return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( + static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( @Autowired(required = false) GrantedAuthorityDefaults grantedAuthorityDefaults) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); if (grantedAuthorityDefaults != null) { @@ -84,4 +91,13 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration { return handler; } + static ReactiveAuthorizationManager manager(ReactiveAuthorizationManager delegate, + ObjectProvider registryProvider) { + ObservationRegistry registry = registryProvider.getIfAvailable(() -> ObservationRegistry.NOOP); + if (registry.isNoop()) { + return delegate; + } + return new ObservationReactiveAuthorizationManager<>(registry, delegate); + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java index 2e30c747a4..9c93ecfb75 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java @@ -16,14 +16,20 @@ package org.springframework.security.config.annotation.method.configuration; +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + import org.springframework.aop.Advisor; -import org.springframework.beans.factory.annotation.Autowired; +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.access.annotation.Secured; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.authorization.method.SecuredAuthorizationManager; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -39,20 +45,26 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy; @Role(BeanDefinition.ROLE_INFRASTRUCTURE) final class SecuredMethodSecurityConfiguration { - private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder - .getContextHolderStrategy(); - @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor securedAuthorizationMethodInterceptor() { - AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor.secured(); - interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + static Advisor securedAuthorizationMethodInterceptor(ObjectProvider strategyProvider, + ObjectProvider registryProvider) { + SecuredAuthorizationManager secured = new SecuredAuthorizationManager(); + SecurityContextHolderStrategy strategy = strategyProvider + .getIfAvailable(SecurityContextHolder::getContextHolderStrategy); + ObservationRegistry registry = registryProvider.getIfAvailable(() -> ObservationRegistry.NOOP); + AuthorizationManager manager = manager(secured, registry); + AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor + .secured(manager); + interceptor.setSecurityContextHolderStrategy(strategy); return interceptor; } - @Autowired(required = false) - void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { - this.securityContextHolderStrategy = securityContextHolderStrategy; + static AuthorizationManager manager(AuthorizationManager jsr250, ObservationRegistry registry) { + if (registry.isNoop()) { + return jsr250; + } + return new ObservationAuthorizationManager<>(registry, jsr250); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java index ea5c2f1a32..9f7f5c9c5b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java @@ -16,11 +16,14 @@ package org.springframework.security.config.annotation.rsocket; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; @@ -43,6 +46,8 @@ class RSocketSecurityConfiguration { private PasswordEncoder passwordEncoder; + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + @Autowired(required = false) void setAuthenticationManager(ReactiveAuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; @@ -58,6 +63,11 @@ class RSocketSecurityConfiguration { this.passwordEncoder = passwordEncoder; } + @Autowired(required = false) + void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + @Bean(name = RSOCKET_SECURITY_BEAN_NAME) @Scope("prototype") RSocketSecurity rsocketSecurity(ApplicationContext context) { @@ -76,6 +86,9 @@ class RSocketSecurityConfiguration { if (this.passwordEncoder != null) { manager.setPasswordEncoder(this.passwordEncoder); } + if (!this.observationRegistry.isNoop()) { + return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager); + } return manager; } return null; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 5f7535b20c..13697dd0e7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -34,6 +35,7 @@ import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ObservationAuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -2994,7 +2996,14 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder.hasRole('USER')"); Assert.state(this.mappingCount > 0, "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); - return postProcess(this.managerBuilder.build()); + ObservationRegistry registry = getObservationRegistry(); + RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build()); + if (registry.isNoop()) { + return manager; + } + return new ObservationAuthorizationManager<>(registry, manager); } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 70648cca6f..74b8337a4b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.reactive; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; @@ -27,6 +29,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.config.web.server.ServerHttpSecurity; @@ -60,6 +63,8 @@ class ServerHttpSecurityConfiguration { private ReactiveUserDetailsPasswordService userDetailsPasswordService; + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + @Autowired(required = false) private BeanFactory beanFactory; @@ -88,6 +93,11 @@ class ServerHttpSecurityConfiguration { this.userDetailsPasswordService = userDetailsPasswordService; } + @Autowired(required = false) + void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + @Bean static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer( ObjectProvider authenticationPrincipalArgumentResolver) { @@ -143,6 +153,9 @@ class ServerHttpSecurityConfiguration { manager.setPasswordEncoder(this.passwordEncoder); } manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + if (!this.observationRegistry.isNoop()) { + return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager); + } return manager; } return null; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java index 8dfc4da381..1e7b09fe6d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -31,6 +33,7 @@ import org.springframework.messaging.handler.invocation.HandlerMethodArgumentRes import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -68,8 +71,9 @@ final class WebSocketMessageBrokerSecurityConfiguration private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor(); - private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor( - ANY_MESSAGE_AUTHENTICATED); + private AuthorizationManager> authorizationManager = ANY_MESSAGE_AUTHENTICATED; + + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; private ApplicationContext context; @@ -86,12 +90,15 @@ final class WebSocketMessageBrokerSecurityConfiguration @Override public void configureClientInboundChannel(ChannelRegistration registration) { - this.authorizationChannelInterceptor - .setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context)); - this.authorizationChannelInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + AuthorizationManager> manager = this.authorizationManager; + if (!this.observationRegistry.isNoop()) { + manager = new ObservationAuthorizationManager<>(this.observationRegistry, manager); + } + AuthorizationChannelInterceptor interceptor = new AuthorizationChannelInterceptor(manager); + interceptor.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context)); + interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); this.securityContextChannelInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); - registration.interceptors(this.securityContextChannelInterceptor, this.csrfChannelInterceptor, - this.authorizationChannelInterceptor); + registration.interceptors(this.securityContextChannelInterceptor, this.csrfChannelInterceptor, interceptor); } @Autowired(required = false) @@ -102,7 +109,12 @@ final class WebSocketMessageBrokerSecurityConfiguration @Autowired(required = false) void setAuthorizationManager(AuthorizationManager> authorizationManager) { - this.authorizationChannelInterceptor = new AuthorizationChannelInterceptor(authorizationManager); + this.authorizationManager = authorizationManager; + } + + @Autowired(required = false) + void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; } @Override diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java index ce199e8ea3..9e2b6a8a65 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java @@ -18,6 +18,8 @@ package org.springframework.security.config.authentication; import java.util.Arrays; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -25,6 +27,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ObservationAuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.BeanIds; @@ -43,6 +46,8 @@ public class AuthenticationManagerFactoryBean implements FactoryBean elements)? Alternatively you can use the " + "authentication-manager-ref attribute on your and elements."; @@ -67,7 +72,11 @@ public class AuthenticationManagerFactoryBean implements FactoryBeanasList(provider)); + ProviderManager manager = new ProviderManager(Arrays.asList(provider)); + if (this.observationRegistry.isNoop()) { + return manager; + } + return new ObservationAuthenticationManager(this.observationRegistry, manager); } } @@ -86,6 +95,10 @@ public class AuthenticationManagerFactoryBean implements FactoryBean T getBeanOrNull(Class type) { try { return this.bf.getBean(type); diff --git a/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java index 2855966da2..3baac94204 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java @@ -19,10 +19,12 @@ package org.springframework.security.config.http; import java.util.List; import java.util.Map; +import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; import org.w3c.dom.Element; import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -34,6 +36,7 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.XmlReaderContext; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.config.Elements; import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler; import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; @@ -51,6 +54,8 @@ class AuthorizationFilterParser implements BeanDefinitionParser { private static final String ATT_ACCESS_DECISION_MANAGER_REF = "access-decision-manager-ref"; + private static final String ATT_OBSERVATION_REGISTRY_REF = "observation-registry-ref"; + private static final String ATT_HTTP_METHOD = "method"; private static final String ATT_PATTERN = "pattern"; @@ -127,9 +132,9 @@ class AuthorizationFilterParser implements BeanDefinitionParser { matcherToExpression.put(matcher, authorizationManager.getBeanDefinition()); } BeanDefinitionBuilder mds = BeanDefinitionBuilder - .rootBeanDefinition(RequestMatcherDelegatingAuthorizationManagerFactory.class); - mds.setFactoryMethod("createRequestMatcherDelegatingAuthorizationManager"); - mds.addConstructorArgValue(matcherToExpression); + .rootBeanDefinition(RequestMatcherDelegatingAuthorizationManagerFactory.class) + .addPropertyValue("requestMatcherMap", matcherToExpression) + .addPropertyValue("observationRegistry", getObservationRegistry(element)); return context.registerWithGeneratedName(mds.getBeanDefinition()); } @@ -169,17 +174,48 @@ class AuthorizationFilterParser implements BeanDefinitionParser { return !StringUtils.hasText(useExpressions) || "true".equals(useExpressions); } - private static class RequestMatcherDelegatingAuthorizationManagerFactory { + private BeanMetadataElement getObservationRegistry(Element methodSecurityElmt) { + String holderStrategyRef = methodSecurityElmt.getAttribute(ATT_OBSERVATION_REGISTRY_REF); + if (StringUtils.hasText(holderStrategyRef)) { + return new RuntimeBeanReference(holderStrategyRef); + } + return BeanDefinitionBuilder.rootBeanDefinition(ObservationRegistryFactory.class).getBeanDefinition(); + } - private static AuthorizationManager createRequestMatcherDelegatingAuthorizationManager( - Map> beans) { + public static final class RequestMatcherDelegatingAuthorizationManagerFactory + implements FactoryBean> { + + private Map> beans; + + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + + @Override + public AuthorizationManager getObject() throws Exception { RequestMatcherDelegatingAuthorizationManager.Builder builder = RequestMatcherDelegatingAuthorizationManager .builder(); - for (Map.Entry> entry : beans + for (Map.Entry> entry : this.beans .entrySet()) { builder.add(entry.getKey(), entry.getValue()); } - return builder.add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated()).build(); + AuthorizationManager manager = builder + .add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated()).build(); + if (!this.observationRegistry.isNoop()) { + return new ObservationAuthorizationManager<>(this.observationRegistry, manager); + } + return manager; + } + + @Override + public Class getObjectType() { + return AuthorizationManager.class; + } + + public void setRequestMatcherMap(Map> beans) { + this.beans = beans; + } + + public void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; } } @@ -199,4 +235,18 @@ class AuthorizationFilterParser implements BeanDefinitionParser { } + static class ObservationRegistryFactory implements FactoryBean { + + @Override + public ObservationRegistry getObject() throws Exception { + return ObservationRegistry.NOOP; + } + + @Override + public Class getObjectType() { + return ObservationRegistry.class; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 1dd528fbff..44c6007c19 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -20,12 +20,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Element; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -43,8 +45,11 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.core.OrderComparator; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; +import org.springframework.security.authentication.ObservationAuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.Elements; @@ -70,6 +75,8 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { private static final String ATT_AUTHENTICATION_MANAGER_REF = "authentication-manager-ref"; + private static final String ATT_OBSERVATION_REGISTRY_REF = "observation-registry-ref"; + static final String ATT_REQUEST_MATCHER_REF = "request-matcher-ref"; static final String ATT_PATH_PATTERN = "pattern"; @@ -246,7 +253,8 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { private BeanReference createAuthenticationManager(Element element, ParserContext pc, ManagedList authenticationProviders) { String parentMgrRef = element.getAttribute(ATT_AUTHENTICATION_MANAGER_REF); - BeanDefinitionBuilder authManager = BeanDefinitionBuilder.rootBeanDefinition(ProviderManager.class); + BeanDefinitionBuilder authManager = BeanDefinitionBuilder + .rootBeanDefinition(ChildAuthenticationManagerFactoryBean.class); authManager.addConstructorArgValue(authenticationProviders); if (StringUtils.hasText(parentMgrRef)) { RuntimeBeanReference parentAuthManager = new RuntimeBeanReference(parentMgrRef); @@ -273,6 +281,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { // gh-6009 authManager.addPropertyValue("authenticationEventPublisher", new RootBeanDefinition(DefaultAuthenticationEventPublisher.class)); + authManager.addPropertyValue("observationRegistry", getObservationRegistry(element)); authManager.getRawBeanDefinition().setSource(pc.extractSource(element)); BeanDefinition authMgrBean = authManager.getBeanDefinition(); String id = pc.getReaderContext().generateBeanName(authMgrBean); @@ -368,6 +377,14 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { registry.registerBeanDefinition(requestRejectedPostProcessorName, requestRejectedBean); } + private static BeanMetadataElement getObservationRegistry(Element methodSecurityElmt) { + String holderStrategyRef = methodSecurityElmt.getAttribute(ATT_OBSERVATION_REGISTRY_REF); + if (StringUtils.hasText(holderStrategyRef)) { + return new RuntimeBeanReference(holderStrategyRef); + } + return BeanDefinitionBuilder.rootBeanDefinition(ObservationRegistryFactory.class).getBeanDefinition(); + } + static class RequestRejectedHandlerPostProcessor implements BeanDefinitionRegistryPostProcessor { private final String beanName; @@ -434,4 +451,62 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { } + public static final class ChildAuthenticationManagerFactoryBean implements FactoryBean { + + private final ProviderManager delegate; + + private AuthenticationEventPublisher authenticationEventPublisher = new DefaultAuthenticationEventPublisher(); + + private boolean eraseCredentialsAfterAuthentication = true; + + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + + public ChildAuthenticationManagerFactoryBean(List providers, + AuthenticationManager parent) { + this.delegate = new ProviderManager(providers, parent); + } + + @Override + public AuthenticationManager getObject() throws Exception { + this.delegate.setAuthenticationEventPublisher(this.authenticationEventPublisher); + this.delegate.setEraseCredentialsAfterAuthentication(this.eraseCredentialsAfterAuthentication); + if (!this.observationRegistry.isNoop()) { + return new ObservationAuthenticationManager(this.observationRegistry, this.delegate); + } + return this.delegate; + } + + @Override + public Class getObjectType() { + return AuthenticationManager.class; + } + + public void setEraseCredentialsAfterAuthentication(boolean eraseCredentialsAfterAuthentication) { + this.eraseCredentialsAfterAuthentication = eraseCredentialsAfterAuthentication; + } + + public void setAuthenticationEventPublisher(AuthenticationEventPublisher authenticationEventPublisher) { + this.authenticationEventPublisher = authenticationEventPublisher; + } + + public void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + } + + static class ObservationRegistryFactory implements FactoryBean { + + @Override + public ObservationRegistry getObject() throws Exception { + return ObservationRegistry.NOOP; + } + + @Override + public Class getObjectType() { + return ObservationRegistry.class; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java index 63f3614536..c1bedb3471 100644 --- a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Element; @@ -42,14 +44,18 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.Jsr250AuthorizationManager; import org.springframework.security.authorization.method.MethodExpressionAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; +import org.springframework.security.authorization.method.SecuredAuthorizationManager; import org.springframework.security.config.Elements; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.core.context.SecurityContextHolder; @@ -75,6 +81,8 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser private static final String ATT_AUTHORIZATION_MGR = "authorization-manager-ref"; + private static final String ATT_OBSERVATION_REGISTRY_REF = "observation-registry-ref"; + private static final String ATT_ACCESS = "access"; private static final String ATT_EXPRESSION = "expression"; @@ -89,6 +97,7 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser pc.extractSource(element)); pc.pushContainingComponent(compositeDef); BeanMetadataElement securityContextHolderStrategy = getSecurityContextHolderStrategy(element); + BeanMetadataElement observationRegistry = getObservationRegistry(element); boolean prePostAnnotationsEnabled = !element.hasAttribute(ATT_USE_PREPOST) || "true".equals(element.getAttribute(ATT_USE_PREPOST)); boolean useAspectJ = "aspectj".equals(element.getAttribute(ATT_MODE)); @@ -100,11 +109,13 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser BeanDefinitionBuilder preAuthorizeInterceptor = BeanDefinitionBuilder .rootBeanDefinition(PreAuthorizeAuthorizationMethodInterceptor.class) .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy); + .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) + .addPropertyValue("observationRegistry", observationRegistry); BeanDefinitionBuilder postAuthorizeInterceptor = BeanDefinitionBuilder .rootBeanDefinition(PostAuthorizeAuthorizationMethodInterceptor.class) .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy); + .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) + .addPropertyValue("observationRegistry", observationRegistry); BeanDefinitionBuilder postFilterInterceptor = BeanDefinitionBuilder .rootBeanDefinition(PostFilterAuthorizationMethodInterceptor.class) .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) @@ -137,10 +148,10 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser boolean securedEnabled = "true".equals(element.getAttribute(ATT_USE_SECURED)); if (securedEnabled) { BeanDefinitionBuilder securedInterceptor = BeanDefinitionBuilder - .rootBeanDefinition(AuthorizationManagerBeforeMethodInterceptor.class) + .rootBeanDefinition(SecuredAuthorizationMethodInterceptor.class) .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) - .setFactoryMethod("secured"); + .addPropertyValue("observationRegistry", observationRegistry); pc.getRegistry().registerBeanDefinition("securedAuthorizationMethodInterceptor", securedInterceptor.getBeanDefinition()); } @@ -149,7 +160,8 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser BeanDefinitionBuilder jsr250Interceptor = BeanDefinitionBuilder .rootBeanDefinition(Jsr250AuthorizationMethodInterceptor.class) .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy); + .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) + .addPropertyValue("observationRegistry", observationRegistry); pc.getRegistry().registerBeanDefinition("jsr250AuthorizationMethodInterceptor", jsr250Interceptor.getBeanDefinition()); } @@ -182,6 +194,14 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser return null; } + private BeanMetadataElement getObservationRegistry(Element methodSecurityElmt) { + String holderStrategyRef = methodSecurityElmt.getAttribute(ATT_OBSERVATION_REGISTRY_REF); + if (StringUtils.hasText(holderStrategyRef)) { + return new RuntimeBeanReference(holderStrategyRef); + } + return BeanDefinitionBuilder.rootBeanDefinition(ObservationRegistryFactory.class).getBeanDefinition(); + } + private BeanMetadataElement getSecurityContextHolderStrategy(Element methodSecurityElmt) { String holderStrategyRef = methodSecurityElmt.getAttribute(ATT_SECURITY_CONTEXT_HOLDER_STRATEGY_REF); if (StringUtils.hasText(holderStrategyRef)) { @@ -295,12 +315,18 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private final Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); @Override public AuthorizationManagerBeforeMethodInterceptor getObject() { + AuthorizationManager manager = this.manager; + if (!this.observationRegistry.isNoop()) { + manager = new ObservationAuthorizationManager<>(this.observationRegistry, this.manager); + } AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor - .jsr250(this.manager); + .jsr250(manager); interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); return interceptor; } @@ -325,6 +351,47 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser this.securityContextHolderStrategy = securityContextHolderStrategy; } + public void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + } + + public static final class SecuredAuthorizationMethodInterceptor + implements FactoryBean { + + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder + .getContextHolderStrategy(); + + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + + private final SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + + @Override + public AuthorizationManagerBeforeMethodInterceptor getObject() { + AuthorizationManager manager = this.manager; + if (!this.observationRegistry.isNoop()) { + manager = new ObservationAuthorizationManager<>(this.observationRegistry, this.manager); + } + AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor + .secured(manager); + interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + return interceptor; + } + + @Override + public Class getObjectType() { + return AuthorizationManagerBeforeMethodInterceptor.class; + } + + public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { + this.securityContextHolderStrategy = securityContextHolderStrategy; + } + + public void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + } public static final class PreAuthorizeAuthorizationMethodInterceptor @@ -333,12 +400,18 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private final PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); @Override public AuthorizationManagerBeforeMethodInterceptor getObject() { + AuthorizationManager manager = this.manager; + if (!this.observationRegistry.isNoop()) { + manager = new ObservationAuthorizationManager<>(this.observationRegistry, this.manager); + } AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(this.manager); + .preAuthorize(manager); interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); return interceptor; } @@ -356,6 +429,10 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser this.manager.setExpressionHandler(expressionHandler); } + public void setObservationRegistry(ObservationRegistry registry) { + this.observationRegistry = registry; + } + } public static final class PostAuthorizeAuthorizationMethodInterceptor @@ -364,12 +441,18 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private final PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); @Override public AuthorizationManagerAfterMethodInterceptor getObject() { + AuthorizationManager manager = this.manager; + if (!this.observationRegistry.isNoop()) { + manager = new ObservationAuthorizationManager<>(this.observationRegistry, this.manager); + } AuthorizationManagerAfterMethodInterceptor interceptor = AuthorizationManagerAfterMethodInterceptor - .postAuthorize(this.manager); + .postAuthorize(manager); interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); return interceptor; } @@ -387,6 +470,10 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser this.manager.setExpressionHandler(expressionHandler); } + public void setObservationRegistry(ObservationRegistry registry) { + this.observationRegistry = registry; + } + } static class SecurityContextHolderStrategyFactory implements FactoryBean { @@ -403,4 +490,18 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser } + static class ObservationRegistryFactory implements FactoryBean { + + @Override + public ObservationRegistry getObject() throws Exception { + return ObservationRegistry.NOOP; + } + + @Override + public Class getObjectType() { + return ObservationRegistry.class; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 994a48e321..16e232a76e 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -31,6 +31,7 @@ import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; +import io.micrometer.observation.ObservationRegistry; import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -50,6 +51,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.config.Customizer; import org.springframework.security.core.Authentication; @@ -1549,6 +1551,14 @@ public class ServerHttpSecurity { return this.context.getBean(beanClass); } + private T getBeanOrDefault(Class beanClass, T defaultInstance) { + T bean = getBeanOrNull(beanClass); + if (bean == null) { + return defaultInstance; + } + return bean; + } + private T getBeanOrNull(Class beanClass) { return getBeanOrNull(ResolvableType.forClass(beanClass)); } @@ -1623,7 +1633,12 @@ public class ServerHttpSecurity { protected void configure(ServerHttpSecurity http) { Assert.state(this.matcher == null, () -> "The matcher " + this.matcher + " does not have an access rule defined"); - AuthorizationWebFilter result = new AuthorizationWebFilter(this.managerBldr.build()); + ReactiveAuthorizationManager manager = this.managerBldr.build(); + ObservationRegistry registry = getBeanOrDefault(ObservationRegistry.class, ObservationRegistry.NOOP); + if (!registry.isNoop()) { + manager = new ObservationReactiveAuthorizationManager<>(registry, manager); + } + AuthorizationWebFilter result = new AuthorizationWebFilter(manager); http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION); } diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc index 786b0777d7..7f89ced5af 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc @@ -222,6 +222,9 @@ method-security.attlist &= method-security.attlist &= ## Specifies the security context holder strategy to use, by default uses a ThreadLocal-based strategy attribute security-context-holder-strategy-ref {xsd:string}? +method-security.attlist &= + ## Use this ObservationRegistry to collect metrics on various parts of the filter chain + attribute observation-registry-ref {xsd:token}? global-method-security = ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. @@ -396,6 +399,9 @@ http.attlist &= name? http.attlist &= authentication-manager-ref? +http.attlist &= + ## Use this ObservationRegistry to collect metrics on various parts of the filter chain + attribute observation-registry-ref {xsd:token}? access-denied-handler = ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. @@ -1057,6 +1063,9 @@ authman.attlist &= authman.attlist &= ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. attribute erase-credentials {xsd:boolean}? +authman.attlist &= + ## Use this ObservationRegistry to collect metrics on various parts of the filter chain + attribute observation-registry-ref {xsd:token}? authentication-provider = ## Indicates that the contained user-service should be used as an authentication source. diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd index 32ca4c6192..f123ad830a 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd @@ -695,6 +695,12 @@ + + + Use this ObservationRegistry to collect metrics on various parts of the filter chain + + + @@ -1389,6 +1395,12 @@ + + + Use this ObservationRegistry to collect metrics on various parts of the filter chain + + + @@ -2990,6 +3002,12 @@ + + + Use this ObservationRegistry to collect metrics on various parts of the filter chain + + + diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java new file mode 100644 index 0000000000..7756506a08 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import io.micrometer.observation.Observation; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link Observation.Context} used during authentications + * + * @author Josh Cummings + * @since 6.0 + */ +public class AuthenticationObservationContext extends Observation.Context { + + private Authentication authenticationRequest; + + private Class authenticationManager; + + private Authentication authenticationResult; + + /** + * Get the {@link Authentication} request that was observed + * @return the observed {@link Authentication} request + */ + public Authentication getAuthenticationRequest() { + return this.authenticationRequest; + } + + /** + * Set the {@link Authentication} request that was observed + * @param authenticationRequest the observed {@link Authentication} request + */ + public void setAuthenticationRequest(Authentication authenticationRequest) { + Assert.notNull(authenticationRequest, "authenticationRequest cannot be null"); + this.authenticationRequest = authenticationRequest; + } + + /** + * Get the {@link Authentication} result that was observed + * + *

+ * Note that if authentication failed, no {@link Authentication} result can be + * observed. In that case, this returns {@code null}. + * @return any observed {@link Authentication} result, {@code null} otherwise + */ + public Authentication getAuthenticationResult() { + return this.authenticationResult; + } + + /** + * Set the {@link Authentication} result that was observed + * @param authenticationResult the observed {@link Authentication} result + */ + public void setAuthenticationResult(Authentication authenticationResult) { + this.authenticationResult = authenticationResult; + } + + /** + * Get the {@link AuthenticationManager} class that processed the authentication + * @return the observed {@link AuthenticationManager} class + */ + public Class getAuthenticationManagerClass() { + return this.authenticationManager; + } + + /** + * Set the {@link AuthenticationManager} class that processed the authentication + * @param authenticationManagerClass the observed {@link AuthenticationManager} class + */ + public void setAuthenticationManagerClass(Class authenticationManagerClass) { + Assert.notNull(authenticationManagerClass, "authenticationManagerClass class cannot be null"); + this.authenticationManager = authenticationManagerClass; + } + +} diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java new file mode 100644 index 0000000000..0d229ad05b --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import org.jetbrains.annotations.NotNull; + +import org.springframework.lang.NonNull; + +/** + * An {@link ObservationConvention} for translating authentications into + * {@link KeyValues}. + * + * @author Josh Cummings + * @since 6.0 + */ +public final class AuthenticationObservationConvention + implements ObservationConvention { + + static final String OBSERVATION_NAME = "spring.security.authentications"; + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return OBSERVATION_NAME; + } + + /** + * {@inheritDoc} + */ + @NotNull + @Override + public KeyValues getLowCardinalityKeyValues(@NonNull AuthenticationObservationContext context) { + return KeyValues.of("authentication.request.type", getAuthenticationType(context)) + .and("authentication.method", getAuthenticationMethod(context)) + .and("authentication.result.type", getAuthenticationResult(context)) + .and("authentication.failure.type", getAuthenticationFailureType(context)); + } + + private String getAuthenticationType(AuthenticationObservationContext context) { + if (context.getAuthenticationRequest() == null) { + return "unknown"; + } + return context.getAuthenticationRequest().getClass().getSimpleName(); + } + + private String getAuthenticationMethod(AuthenticationObservationContext context) { + if (context.getAuthenticationManagerClass() == null) { + return "unknown"; + } + return context.getAuthenticationManagerClass().getSimpleName(); + } + + private String getAuthenticationResult(AuthenticationObservationContext context) { + if (context.getAuthenticationResult() == null) { + return "n/a"; + } + return context.getAuthenticationResult().getClass().getSimpleName(); + } + + private String getAuthenticationFailureType(AuthenticationObservationContext context) { + if (context.getError() == null) { + return "n/a"; + } + return context.getError().getClass().getSimpleName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsContext(@NotNull Observation.Context context) { + return context instanceof AuthenticationObservationContext; + } + +} diff --git a/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java new file mode 100644 index 0000000000..377761be2b --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.util.Assert; + +/** + * An {@link AuthenticationManager} that observes the authentication + * + * @author Josh Cummings + * @since 6.0 + */ +public final class ObservationAuthenticationManager implements AuthenticationManager { + + private final ObservationRegistry registry; + + private final AuthenticationManager delegate; + + private final AuthenticationObservationConvention convention = new AuthenticationObservationConvention(); + + public ObservationAuthenticationManager(ObservationRegistry registry, AuthenticationManager delegate) { + Assert.notNull(registry, "observationRegistry cannot be null"); + Assert.notNull(delegate, "authenticationManager cannot be null"); + this.registry = registry; + this.delegate = delegate; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + AuthenticationObservationContext context = new AuthenticationObservationContext(); + context.setAuthenticationRequest(authentication); + context.setAuthenticationManagerClass(this.delegate.getClass()); + return Observation.createNotStarted(this.convention, () -> context, this.registry).observe(() -> { + Authentication result = this.delegate.authenticate(authentication); + context.setAuthenticationResult(result); + return result; + }); + } + +} diff --git a/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java new file mode 100644 index 0000000000..6ae124e39c --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import reactor.core.publisher.Mono; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +/** + * An {@link ReactiveAuthenticationManager} that observes the authentication + * + * @author Josh Cummings + * @since 6.0 + */ +public class ObservationReactiveAuthenticationManager implements ReactiveAuthenticationManager { + + private final ObservationRegistry registry; + + private final ReactiveAuthenticationManager delegate; + + private final AuthenticationObservationConvention convention = new AuthenticationObservationConvention(); + + public ObservationReactiveAuthenticationManager(ObservationRegistry registry, + ReactiveAuthenticationManager delegate) { + this.registry = registry; + this.delegate = delegate; + } + + @Override + public Mono authenticate(Authentication authentication) throws AuthenticationException { + AuthenticationObservationContext context = new AuthenticationObservationContext(); + context.setAuthenticationRequest(authentication); + context.setAuthenticationManagerClass(this.delegate.getClass()); + Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry).start(); + return this.delegate.authenticate(authentication).doOnSuccess((result) -> { + context.setAuthenticationResult(result); + observation.stop(); + }).doOnCancel(observation::stop).doOnError((t) -> { + observation.error(t); + observation.stop(); + }); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java new file mode 100644 index 0000000000..8e5692213c --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import io.micrometer.observation.Observation; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link Observation.Context} used during authorizations + * + * @author Josh Cummings + * @since 6.0 + */ +public class AuthorizationObservationContext extends Observation.Context { + + private Authentication authentication; + + private final T object; + + private AuthorizationDecision decision; + + public AuthorizationObservationContext(T object) { + Assert.notNull(object, "object cannot be null"); + this.object = object; + } + + /** + * Get the observed {@link Authentication} for this authorization + * + *

+ * Note that if the authorization did not require inspecting the + * {@link Authentication}, this will return {@code null}. + * @return any observed {@link Authentication}, {@code null} otherwise + */ + public Authentication getAuthentication() { + return this.authentication; + } + + /** + * Set the observed {@link Authentication} for this authorization + * @param authentication the observed {@link Authentication} + */ + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + + /** + * Get the object for which access was requested + * @return the requested object + */ + public T getObject() { + return this.object; + } + + /** + * Get the observed {@link AuthorizationDecision} + * @return the observed {@link AuthorizationDecision} + */ + public AuthorizationDecision getDecision() { + return this.decision; + } + + /** + * Set the observed {@link AuthorizationDecision} + * @param decision the observed {@link AuthorizationDecision} + */ + public void setDecision(AuthorizationDecision decision) { + this.decision = decision; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java new file mode 100644 index 0000000000..8e2f6f8499 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * An {@link ObservationConvention} for translating authorizations into {@link KeyValues}. + * + * @author Josh Cummings + * @since 6.0 + */ +public final class AuthorizationObservationConvention + implements ObservationConvention> { + + static final String OBSERVATION_NAME = "spring.security.authorizations"; + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return OBSERVATION_NAME; + } + + /** + * {@inheritDoc} + */ + @Override + public KeyValues getLowCardinalityKeyValues(AuthorizationObservationContext context) { + return KeyValues.of("authentication.type", getAuthenticationType(context)) + .and("object.type", getObjectType(context)) + .and("authorization.decision", getAuthorizationDecision(context)); + } + + /** + * {@inheritDoc} + */ + @Override + public KeyValues getHighCardinalityKeyValues(AuthorizationObservationContext context) { + return KeyValues.of("authentication.authorities", getAuthorities(context)).and("authorization.decision.details", + getDecisionDetails(context)); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof AuthorizationObservationContext; + } + + private String getAuthenticationType(AuthorizationObservationContext context) { + if (context.getAuthentication() == null) { + return "n/a"; + } + return context.getAuthentication().getClass().getSimpleName(); + } + + private String getObjectType(AuthorizationObservationContext context) { + if (context.getObject() == null) { + return "unknown"; + } + return context.getObject().getClass().getSimpleName(); + } + + private String getAuthorizationDecision(AuthorizationObservationContext context) { + if (context.getDecision() == null) { + return "unknown"; + } + return String.valueOf(context.getDecision().isGranted()); + } + + private String getAuthorities(AuthorizationObservationContext context) { + if (context.getAuthentication() == null) { + return "n/a"; + } + return String.valueOf(context.getAuthentication().getAuthorities()); + } + + private String getDecisionDetails(AuthorizationObservationContext context) { + if (context.getDecision() == null) { + return "unknown"; + } + AuthorizationDecision decision = context.getDecision(); + return String.valueOf(decision); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java new file mode 100644 index 0000000000..343762e0ac --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.function.Supplier; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; + +/** + * An {@link AuthorizationManager} that observes the authorization + * + * @author Josh Cummings + * @since 6.0 + */ +public final class ObservationAuthorizationManager implements AuthorizationManager { + + private final ObservationRegistry registry; + + private final AuthorizationManager delegate; + + private final AuthorizationObservationConvention convention = new AuthorizationObservationConvention(); + + public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager delegate) { + this.registry = registry; + this.delegate = delegate; + } + + @Override + public AuthorizationDecision check(Supplier authentication, T object) { + AuthorizationObservationContext context = new AuthorizationObservationContext<>(object); + Supplier wrapped = () -> { + context.setAuthentication(authentication.get()); + return context.getAuthentication(); + }; + Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry).start(); + try (Observation.Scope scope = observation.openScope()) { + AuthorizationDecision decision = this.delegate.check(wrapped, object); + context.setDecision(decision); + if (decision != null && !decision.isGranted()) { + observation.error(new AccessDeniedException("Access Denied")); + } + return decision; + } + catch (Throwable ex) { + observation.error(ex); + throw ex; + } + finally { + observation.stop(); + } + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java new file mode 100644 index 0000000000..ada6ff4270 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import reactor.core.publisher.Mono; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; + +/** + * An {@link ReactiveAuthorizationManager} that observes the authentication + * + * @author Josh Cummings + * @since 6.0 + */ +public final class ObservationReactiveAuthorizationManager implements ReactiveAuthorizationManager { + + private final ObservationRegistry registry; + + private final ReactiveAuthorizationManager delegate; + + private final AuthorizationObservationConvention convention = new AuthorizationObservationConvention(); + + public ObservationReactiveAuthorizationManager(ObservationRegistry registry, + ReactiveAuthorizationManager delegate) { + this.registry = registry; + this.delegate = delegate; + } + + @Override + public Mono check(Mono authentication, T object) { + AuthorizationObservationContext context = new AuthorizationObservationContext<>(object); + Mono wrapped = authentication.map((auth) -> { + context.setAuthentication(auth); + return context.getAuthentication(); + }); + Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry).start(); + return this.delegate.check(wrapped, object).doOnSuccess((decision) -> { + context.setDecision(decision); + if (decision == null || !decision.isGranted()) { + observation.error(new AccessDeniedException("Access Denied")); + } + observation.stop(); + }).doOnCancel(observation::stop).doOnError((t) -> { + observation.error(t); + observation.stop(); + }); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java index fcdad71b1c..7728149a71 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java @@ -98,6 +98,20 @@ public final class AuthorizationManagerAfterMethodInterceptor return interceptor; } + /** + * Creates an interceptor for the {@link PostAuthorize} annotation + * @param authorizationManager the {@link AuthorizationManager} to use + * @return the interceptor + * @since 6.0 + */ + public static AuthorizationManagerAfterMethodInterceptor postAuthorize( + AuthorizationManager authorizationManager) { + AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor( + AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager); + interceptor.setOrder(500); + return interceptor; + } + /** * Determine if an {@link Authentication} has access to the {@link MethodInvocation} * using the {@link AuthorizationManager}. diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java index 08aae6aca9..955e3eb434 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java @@ -102,6 +102,20 @@ public final class AuthorizationManagerBeforeMethodInterceptor return interceptor; } + /** + * Creates an interceptor for the {@link PreAuthorize} annotation + * @param authorizationManager the {@link AuthorizationManager} to use + * @return the interceptor + * @since 6.0 + */ + public static AuthorizationManagerBeforeMethodInterceptor preAuthorize( + AuthorizationManager authorizationManager) { + AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor( + AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder()); + return interceptor; + } + /** * Creates an interceptor for the {@link Secured} annotation * @return the interceptor @@ -123,6 +137,20 @@ public final class AuthorizationManagerBeforeMethodInterceptor return interceptor; } + /** + * Creates an interceptor for the {@link Secured} annotation + * @param authorizationManager the {@link AuthorizationManager} to use + * @return the interceptor + * @since 6.0 + */ + public static AuthorizationManagerBeforeMethodInterceptor secured( + AuthorizationManager authorizationManager) { + AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor( + AuthorizationMethodPointcuts.forAnnotations(Secured.class), authorizationManager); + interceptor.setOrder(AuthorizationInterceptorsOrder.SECURED.getOrder()); + return interceptor; + } + /** * Creates an interceptor for the JSR-250 annotations * @return the interceptor @@ -144,6 +172,21 @@ public final class AuthorizationManagerBeforeMethodInterceptor return interceptor; } + /** + * Creates an interceptor for the JSR-250 annotations + * @param authorizationManager the {@link AuthorizationManager} to use + * @return the interceptor + * @since 6.0 + */ + public static AuthorizationManagerBeforeMethodInterceptor jsr250( + AuthorizationManager authorizationManager) { + AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor( + AuthorizationMethodPointcuts.forAnnotations(RolesAllowed.class, DenyAll.class, PermitAll.class), + authorizationManager); + interceptor.setOrder(AuthorizationInterceptorsOrder.JSR250.getOrder()); + return interceptor; + } + /** * Determine if an {@link Authentication} has access to the {@link MethodInvocation} * using the configured {@link AuthorizationManager}. diff --git a/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java new file mode 100644 index 0000000000..4583d61746 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ObservationAuthenticationManager} + */ +public class ObservationAuthenticationManagerTests { + + private ObservationRegistry registry; + + private ObservationHandler handler; + + private AuthenticationManager authenticationManager; + + private ObservationAuthenticationManager tested; + + private final Authentication token = new TestingAuthenticationToken("user", "pass"); + + private final Authentication authentication = new TestingAuthenticationToken("user", "pass", "app"); + + @BeforeEach + void setup() { + this.handler = mock(ObservationHandler.class); + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(this.handler); + this.registry = registry; + this.authenticationManager = mock(AuthenticationManager.class); + this.tested = new ObservationAuthenticationManager(this.registry, this.authenticationManager); + } + + @Test + void authenticateWhenDefaultsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authenticationManager.authenticate(any())).willReturn(this.authentication); + this.tested.authenticate(this.token); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthenticationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isNull(); + assertThat(captor.getValue()).isInstanceOf(AuthenticationObservationContext.class); + AuthenticationObservationContext context = (AuthenticationObservationContext) captor.getValue(); + assertThat(context.getAuthenticationManagerClass()).isEqualTo(this.authenticationManager.getClass()); + assertThat(context.getAuthenticationRequest()).isEqualTo(this.token); + assertThat(context.getAuthenticationResult()).isEqualTo(this.authentication); + + } + + @Test + void authenticationWhenErrorsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authenticationManager.authenticate(any())).willThrow(BadCredentialsException.class); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.tested.authenticate(this.token)); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthenticationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isInstanceOf(AuthenticationException.class); + assertThat(captor.getValue()).isInstanceOf(AuthenticationObservationContext.class); + AuthenticationObservationContext context = (AuthenticationObservationContext) captor.getValue(); + assertThat(context.getAuthenticationManagerClass()).isEqualTo(this.authenticationManager.getClass()); + assertThat(context.getAuthenticationRequest()).isEqualTo(this.token); + assertThat(context.getAuthenticationResult()).isNull(); + } + +} diff --git a/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java new file mode 100644 index 0000000000..e9da7a7024 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ObservationAuthenticationManager} + */ +public class ObservationReactiveAuthenticationManagerTests { + + private ObservationRegistry registry; + + private ObservationHandler handler; + + private ReactiveAuthenticationManager authenticationManager; + + private ObservationReactiveAuthenticationManager tested; + + private final Authentication token = new TestingAuthenticationToken("user", "pass"); + + private final Authentication authentication = new TestingAuthenticationToken("user", "pass", "app"); + + @BeforeEach + void setup() { + this.handler = mock(ObservationHandler.class); + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(this.handler); + this.registry = registry; + this.authenticationManager = mock(ReactiveAuthenticationManager.class); + this.tested = new ObservationReactiveAuthenticationManager(this.registry, this.authenticationManager); + } + + @Test + void authenticateWhenDefaultsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authenticationManager.authenticate(any())).willReturn(Mono.just(this.authentication)); + this.tested.authenticate(this.token).block(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthenticationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isNull(); + assertThat(captor.getValue()).isInstanceOf(AuthenticationObservationContext.class); + AuthenticationObservationContext context = (AuthenticationObservationContext) captor.getValue(); + assertThat(context.getAuthenticationManagerClass()).isEqualTo(this.authenticationManager.getClass()); + assertThat(context.getAuthenticationRequest()).isEqualTo(this.token); + assertThat(context.getAuthenticationResult()).isEqualTo(this.authentication); + + } + + @Test + void authenticationWhenErrorsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authenticationManager.authenticate(any())) + .willReturn(Mono.error(new BadCredentialsException("fail"))); + assertThatExceptionOfType(BadCredentialsException.class) + .isThrownBy(() -> this.tested.authenticate(this.token).block()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthenticationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isInstanceOf(AuthenticationException.class); + assertThat(captor.getValue()).isInstanceOf(AuthenticationObservationContext.class); + AuthenticationObservationContext context = (AuthenticationObservationContext) captor.getValue(); + assertThat(context.getAuthenticationManagerClass()).isEqualTo(this.authenticationManager.getClass()); + assertThat(context.getAuthenticationRequest()).isEqualTo(this.token); + assertThat(context.getAuthenticationResult()).isNull(); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java new file mode 100644 index 0000000000..16204bf50e --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.function.Supplier; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ObservationAuthorizationManager} + */ +public class ObservationAuthorizationManagerTests { + + private ObservationRegistry registry; + + private ObservationHandler handler; + + private AuthorizationManager authorizationManager; + + private ObservationAuthorizationManager tested; + + private final Supplier token = () -> new TestingAuthenticationToken("user", "pass"); + + private final Object object = new Object(); + + private final AuthorizationDecision grant = new AuthorizationDecision(true); + + private final AuthorizationDecision deny = new AuthorizationDecision(false); + + @BeforeEach + void setup() { + this.handler = mock(ObservationHandler.class); + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(this.handler); + this.registry = registry; + this.authorizationManager = mock(AuthorizationManager.class); + this.tested = new ObservationAuthorizationManager<>(this.registry, this.authorizationManager); + } + + @Test + void verifyWhenDefaultsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authorizationManager.check(any(), any())).willReturn(this.grant); + this.tested.verify(this.token, this.object); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isNull(); + assertThat(captor.getValue()).isInstanceOf(AuthorizationObservationContext.class); + AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); + assertThat(context.getAuthentication()).isNull(); + assertThat(context.getObject()).isEqualTo(this.object); + assertThat(context.getDecision()).isEqualTo(this.grant); + } + + @Test + void verifyWhenErrorsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authorizationManager.check(any(), any())).willReturn(this.deny); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.tested.verify(this.token, this.object)); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isInstanceOf(AccessDeniedException.class); + assertThat(captor.getValue()).isInstanceOf(AuthorizationObservationContext.class); + AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); + assertThat(context.getAuthentication()).isNull(); + assertThat(context.getObject()).isEqualTo(this.object); + assertThat(context.getDecision()).isEqualTo(this.deny); + } + + @Test + void verifyWhenLooksUpAuthenticationThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authorizationManager.check(any(), any())).willAnswer((invocation) -> { + ((Supplier) invocation.getArgument(0)).get(); + return this.grant; + }); + this.tested.verify(this.token, this.object); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isNull(); + AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); + assertThat(context.getAuthentication()).isEqualTo(this.token.get()); + assertThat(context.getObject()).isEqualTo(this.object); + assertThat(context.getDecision()).isEqualTo(this.grant); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java new file mode 100644 index 0000000000..044c1c24b1 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ObservationAuthorizationManager} + */ +public class ObservationReactiveAuthorizationManagerTests { + + private ObservationRegistry registry; + + private ObservationHandler handler; + + private ReactiveAuthorizationManager authorizationManager; + + private ObservationReactiveAuthorizationManager tested; + + private final Mono token = Mono.just(new TestingAuthenticationToken("user", "pass")); + + private final Object object = new Object(); + + private final AuthorizationDecision grant = new AuthorizationDecision(true); + + private final AuthorizationDecision deny = new AuthorizationDecision(false); + + @BeforeEach + void setup() { + this.handler = mock(ObservationHandler.class); + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(this.handler); + this.registry = registry; + this.authorizationManager = mock(ReactiveAuthorizationManager.class); + this.tested = new ObservationReactiveAuthorizationManager<>(this.registry, this.authorizationManager); + } + + @Test + void verifyWhenDefaultsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authorizationManager.check(any(), any())).willReturn(Mono.just(this.grant)); + this.tested.verify(this.token, this.object).block(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isNull(); + assertThat(captor.getValue()).isInstanceOf(AuthorizationObservationContext.class); + AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); + assertThat(context.getAuthentication()).isNull(); + assertThat(context.getObject()).isEqualTo(this.object); + assertThat(context.getDecision()).isEqualTo(this.grant); + } + + @Test + void verifyWhenErrorsThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authorizationManager.check(any(), any())).willReturn(Mono.just(this.deny)); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.tested.verify(this.token, this.object).block()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isInstanceOf(AccessDeniedException.class); + assertThat(captor.getValue()).isInstanceOf(AuthorizationObservationContext.class); + AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); + assertThat(context.getAuthentication()).isNull(); + assertThat(context.getObject()).isEqualTo(this.object); + assertThat(context.getDecision()).isEqualTo(this.deny); + } + + @Test + void verifyWhenLooksUpAuthenticationThenObserves() { + given(this.handler.supportsContext(any())).willReturn(true); + given(this.authorizationManager.check(any(), any())).willAnswer((invocation) -> { + ((Mono) invocation.getArgument(0)).block(); + return Mono.just(this.grant); + }); + this.tested.verify(this.token, this.object).block(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.handler).onStart(captor.capture()); + assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); + assertThat(captor.getValue().getError()).isNull(); + AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); + assertThat(context.getAuthentication()).isEqualTo(this.token.block()); + assertThat(context.getObject()).isEqualTo(this.object); + assertThat(context.getDecision()).isEqualTo(this.grant); + } + +}