Make Observations Selectable

Closes gh-15678
This commit is contained in:
Josh Cummings
2024-09-23 13:00:00 -06:00
parent 69e3c248fa
commit d6b620b9f7
25 changed files with 927 additions and 268 deletions

View File

@@ -87,6 +87,7 @@ import org.springframework.security.authorization.method.PrePostTemplateDefaults
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.observation.SecurityObservationSettings;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.test.SpringTestParentApplicationContextExecutionListener;
@@ -114,6 +115,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link PrePostMethodSecurityConfiguration}.
@@ -1062,6 +1064,43 @@ public class PrePostMethodSecurityConfigurationTests {
verify(handler).onError(any());
}
@Test
@WithMockUser
public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class,
SelectableObservationsConfig.class)
.autowire();
this.methodSecurityService.preAuthorizePermitAll();
ObservationHandler<?> handler = this.spring.getContext().getBean(ObservationHandler.class);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize);
verifyNoInteractions(handler);
}
@Test
@WithMockUser
public void securedMethodWhenExcludeAuthorizationObservationsThenUnobserved() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class,
SelectableObservationsConfig.class)
.autowire();
this.methodSecurityService.securedUser();
ObservationHandler<?> handler = this.spring.getContext().getBean(ObservationHandler.class);
verifyNoInteractions(handler);
}
@Test
@WithMockUser
public void jsr250MethodWhenExcludeAuthorizationObservationsThenUnobserved() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class,
SelectableObservationsConfig.class)
.autowire();
this.methodSecurityService.jsr250RolesAllowedUser();
ObservationHandler<?> handler = this.spring.getContext().getBean(ObservationHandler.class);
verifyNoInteractions(handler);
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
@@ -1742,4 +1781,14 @@ public class PrePostMethodSecurityConfigurationTests {
}
@Configuration
static class SelectableObservationsConfig {
@Bean
SecurityObservationSettings observabilityDefaults() {
return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build();
}
}
}

View File

@@ -33,9 +33,11 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
@@ -55,6 +57,7 @@ import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.observation.SecurityObservationSettings;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
@@ -69,6 +72,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* @author Tadaya Tsuyukubo
@@ -260,6 +264,27 @@ public class ReactiveMethodSecurityConfigurationTests {
verify(handler).onError(any());
}
@Test
@WithMockUser
public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() {
this.spring
.register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.class,
SelectableObservationsConfig.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
Authentication user = TestAuthentication.authenticatedUser();
StepVerifier
.create(service.preAuthorizeUser().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
.expectNextCount(1)
.verifyComplete();
ObservationHandler<?> handler = this.spring.getContext().getBean(ObservationHandler.class);
StepVerifier
.create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
.expectError()
.verify();
verifyNoInteractions(handler);
}
private static Consumer<User.UserBuilder> authorities(String... authorities) {
return (builder) -> builder.authorities(authorities);
}
@@ -432,9 +457,37 @@ public class ReactiveMethodSecurityConfigurationTests {
}
@Bean
PrePostMethodSecurityConfigurationTests.ObservationRegistryPostProcessor observationRegistryPostProcessor(
ObservationRegistryPostProcessor observationRegistryPostProcessor(
ObjectProvider<ObservationHandler<Observation.Context>> handler) {
return new PrePostMethodSecurityConfigurationTests.ObservationRegistryPostProcessor(handler);
return new ObservationRegistryPostProcessor(handler);
}
}
static class ObservationRegistryPostProcessor implements BeanPostProcessor {
private final ObjectProvider<ObservationHandler<Observation.Context>> handler;
ObservationRegistryPostProcessor(ObjectProvider<ObservationHandler<Observation.Context>> handler) {
this.handler = handler;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ObservationRegistry registry) {
registry.observationConfig().observationHandler(this.handler.getObject());
}
return bean;
}
}
@Configuration
static class SelectableObservationsConfig {
@Bean
SecurityObservationSettings observabilityDefaults() {
return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build();
}
}

View File

@@ -47,6 +47,7 @@ import org.springframework.security.config.annotation.web.AbstractRequestMatcher
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.observation.SecurityObservationSettings;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
@@ -80,6 +81,7 @@ import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@@ -650,6 +652,18 @@ public class AuthorizeHttpRequestsConfigurerTests {
verify(handler).onError(any());
}
@Test
public void getWhenExcludeAuthorizationObservationsThenUnobserved() throws Exception {
this.spring
.register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class,
SelectableObservationsConfig.class)
.autowire();
ObservationHandler<Observation.Context> handler = this.spring.getContext().getBean(ObservationHandler.class);
this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk());
this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden());
verifyNoInteractions(handler);
}
@Configuration
@EnableWebSecurity
static class GrantedAuthorityDefaultHasRoleConfig {
@@ -1288,4 +1302,14 @@ public class AuthorizeHttpRequestsConfigurerTests {
}
@Configuration
static class SelectableObservationsConfig {
@Bean
SecurityObservationSettings observabilityDefaults() {
return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build();
}
}
}

View File

@@ -39,6 +39,7 @@ import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.observation.SecurityObservationSettings;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.AuthenticationException;
@@ -63,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.security.config.Customizer.withDefaults;
@@ -189,6 +191,26 @@ public class HttpBasicConfigurerTests {
assertThat(context.getValue()).isInstanceOf(AuthenticationObservationContext.class);
}
@Test
public void httpBasicWhenExcludeAuthenticationObservationsThenUnobserved() throws Exception {
this.spring
.register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.class,
SelectableObservationsConfig.class)
.autowire();
ObservationHandler<Observation.Context> handler = this.spring.getContext().getBean(ObservationHandler.class);
this.mvc.perform(get("/").with(httpBasic("user", "password")))
.andExpect(status().isOk())
.andExpect(content().string("user"));
ArgumentCaptor<Observation.Context> context = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, atLeastOnce()).onStart(context.capture());
assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext);
context = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, atLeastOnce()).onStop(context.capture());
assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext);
this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized());
verify(handler, never()).onError(any());
}
@Configuration
@EnableWebSecurity
static class ObjectPostProcessorConfig {
@@ -455,4 +477,14 @@ public class HttpBasicConfigurerTests {
}
@Configuration
static class SelectableObservationsConfig {
@Bean
SecurityObservationSettings observabilityDefaults() {
return SecurityObservationSettings.withDefaults().shouldObserveAuthentications(false).build();
}
}
}

View File

@@ -69,6 +69,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.observation.SecurityObservationSettings;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -106,6 +107,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
public class WebSocketMessageBrokerSecurityConfigurationTests {
@@ -414,6 +416,28 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
verify(observationHandler).onError(any());
}
@Test
public void sendMessageWhenExcludeAuthorizationObservationsThenUnobserved() {
loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.class, SelectableObservationsConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
Message<?> message = message(headers, "/authenticated");
headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
clientInboundChannel().send(message);
ObservationHandler<Observation.Context> observationHandler = this.context.getBean(ObservationHandler.class);
headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
message = message(headers, "/denyAll");
headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
try {
clientInboundChannel().send(message);
}
catch (MessageDeliveryException ex) {
// okay
}
verifyNoInteractions(observationHandler);
}
private void assertHandshake(HttpServletRequest request) {
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
@@ -968,4 +992,14 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
}
@Configuration
static class SelectableObservationsConfig {
@Bean
SecurityObservationSettings observabilityDefaults() {
return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build();
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.observation;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SecurityObservationSettings}
*/
public class SecurityObservationSettingsTests {
@Test
void withDefaultsThenFilterOffAuthenticationOnAuthorizationOn() {
SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults().build();
assertThat(defaults.shouldObserveRequests()).isFalse();
assertThat(defaults.shouldObserveAuthentications()).isTrue();
assertThat(defaults.shouldObserveAuthorizations()).isTrue();
}
@Test
void noObservationsWhenConstructedThenAllOff() {
SecurityObservationSettings defaults = SecurityObservationSettings.noObservations();
assertThat(defaults.shouldObserveRequests()).isFalse();
assertThat(defaults.shouldObserveAuthentications()).isFalse();
assertThat(defaults.shouldObserveAuthorizations()).isFalse();
}
@Test
void withDefaultsWhenExclusionsThenInstanceReflects() {
SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults()
.shouldObserveAuthentications(false)
.shouldObserveAuthorizations(false)
.shouldObserveRequests(true)
.build();
assertThat(defaults.shouldObserveRequests()).isTrue();
assertThat(defaults.shouldObserveAuthentications()).isFalse();
assertThat(defaults.shouldObserveAuthorizations()).isFalse();
}
}