From f3051483d775c61779e23aebcd935bffd920ed87 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Wed, 25 Aug 2021 15:44:45 -0700 Subject: [PATCH] Instrumenting Spring Security (#2011) * First draft of instrumenting Spring Security * Create events based on Authentication not SecurityContext * Simplifying security config * Checkstyle reformat * Create DocumentedSpan and EventValue for Spring Security instrumentation * Adding spring-security-bom to ensure the right dependencies are used. * TracingSecurityContextChangedListenerTests * Adding integration tests * Deleting spring-cloud-sleuth-sample-security * Checkstyle, license, javadoc * Inlining SleuthSecuritySpan methods * Deleting javadoc links from DocumentedSpan * Updating auto-generated docs --- docs/src/main/asciidoc/_spans.adoc | 16 ++ pom.xml | 21 ++- .../TraceSecurityAutoConfiguration.java | 49 ++++++ .../main/resources/META-INF/spring.factories | 1 + spring-cloud-sleuth-instrumentation/pom.xml | 5 + .../security/SleuthSecuritySpan.java | 81 ++++++++++ ...TracingSecurityContextChangedListener.java | 91 +++++++++++ ...ngSecurityContextChangedListenerTests.java | 123 +++++++++++++++ tests/brave/pom.xml | 1 + .../pom.xml | 81 ++++++++++ ...ontextChangedListenerIntegrationTests.java | 56 +++++++ .../src/test/resources/application.yml | 3 + tests/common/pom.xml | 5 + .../security/SpringSecurityTests.java | 146 ++++++++++++++++++ 14 files changed, 672 insertions(+), 7 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc index bca12b758..6088775f8 100644 --- a/docs/src/main/asciidoc/_spans.adoc +++ b/docs/src/main/asciidoc/_spans.adoc @@ -532,6 +532,22 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in |method|Method name that got annotated with @Scheduled. |=== +=== Security Context Change + +> Indicates that a SecurityContextChangedEvent happened during the current span. + +**Span name** `Security Context Change`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan` + +.Event Values +|=== +|Name | Description +|Authentication cleared %s|Event created when an Authentication object is removed from the SecurityContext. (since the name contains `%s` the final value will be resolved at runtime) +|Authentication replaced %s|Event created when an Authentication object is replaced with a new one in the SecurityContext. (since the name contains `%s` the final value will be resolved at runtime) +|Authentication set %s|Event created when an Authentication object is added to the SecurityContext. (since the name contains `%s` the final value will be resolved at runtime) +|=== + === Session Create Span > Span created when a new session has to be created. diff --git a/pom.xml b/pom.xml index 31bcb1582..bdac48726 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,8 @@ 5.13.2 0.32.0 2.3.4.RELEASE + 2.2.0.RELEASE + 5.6.0-M2 false 4.9.0 4.8.0 @@ -83,7 +85,6 @@ 1.7.1 3.3.0 3.0.1 - 2.2.0.RELEASE 3.9.1 1.7 10.0.6 @@ -267,11 +268,22 @@ import pom + + org.springframework.security + spring-security-bom + ${spring-security-version} + pom + import + org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure ${spring-security-boot-autoconfigure.version} - true + + + org.springframework.security.oauth + spring-security-oauth2 + ${spring-security-oauth2.version} cglib @@ -289,11 +301,6 @@ mockwebserver ${mockwebserver.version} - - org.springframework.security.oauth - spring-security-oauth2 - ${spring-security-oauth2.version} - io.zipkin.aws brave-propagation-aws diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java new file mode 100644 index 000000000..bb88dbad0 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.autoconfig.instrument.security; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.security.TracingSecurityContextChangedListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.context.SecurityContextChangedListener; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Security. + * + * @author Jonatan Ivanov + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(SecurityContextChangedListener.class) +@ConditionalOnProperty(value = "spring.sleuth.security.enabled", matchIfMissing = true) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceSecurityAutoConfiguration { + + @Bean + public TracingSecurityContextChangedListener tracingSecurityContextChangedListener(Tracer tracer) { + return new TracingSecurityContextChangedListener(tracer); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index fdeb8e939..d931b5211 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -22,6 +22,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.web.client.feign.TraceFei org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.scheduling.TraceSchedulingAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.session.TraceSessionAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.security.TraceSecurityAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index f5d44ce63..11628a9b7 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -212,6 +212,11 @@ spring-session-data-redis true + + org.springframework.security + spring-security-core + true + io.projectreactor.kotlin reactor-kotlin-extensions diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java new file mode 100644 index 000000000..1b77df1cb --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.instrument.security; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; + +/** + * DocumentedSpan for Spring Security Instrumentation. + * + * @author Jonatan Ivanov + * @since 3.1.0 + */ +enum SleuthSecuritySpan implements DocumentedSpan { + + /** + * Indicates that a SecurityContextChangedEvent happened during the current span. + */ + SECURITY_CONTEXT_CHANGE { + @Override + public String getName() { + return "Security Context Change"; + } + + @Override + public EventValue[] getEvents() { + return SleuthSecurityEvent.values(); + } + }; + + enum SleuthSecurityEvent implements EventValue { + + /** + * Event created when an Authentication object is added to the SecurityContext. + */ + AUTHENTICATION_SET { + @Override + public String getValue() { + return "Authentication set %s"; + } + }, + + /** + * Event created when an Authentication object is replaced with a new one in the + * SecurityContext. + */ + AUTHENTICATION_REPLACED { + @Override + public String getValue() { + return "Authentication replaced %s"; + } + }, + + /** + * Event created when an Authentication object is removed from the + * SecurityContext. + */ + AUTHENTICATION_CLEARED { + @Override + public String getValue() { + return "Authentication cleared %s"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java new file mode 100644 index 000000000..e9220690a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.instrument.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextChangedEvent; +import org.springframework.security.core.context.SecurityContextChangedListener; + +import static java.lang.String.format; +import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SECURITY_CONTEXT_CHANGE; +import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent; +import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent.AUTHENTICATION_CLEARED; +import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent.AUTHENTICATION_REPLACED; +import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent.AUTHENTICATION_SET; + +/** + * {@link SecurityContextChangedListener} that adds tracing support for Spring Security. + * + * @author Jonatan Ivanov + * @since 3.1.0 + */ +public class TracingSecurityContextChangedListener implements SecurityContextChangedListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(TracingSecurityContextChangedListener.class); + + private final Tracer tracer; + + public TracingSecurityContextChangedListener(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public void securityContextChanged(SecurityContextChangedEvent securityContextChangedEvent) { + SecurityContext previousContext = securityContextChangedEvent.getPreviousContext(); + SecurityContext currentContext = securityContextChangedEvent.getCurrentContext(); + Authentication previousAuthentication = previousContext != null ? previousContext.getAuthentication() : null; + Authentication currentAuthentication = currentContext != null ? currentContext.getAuthentication() : null; + + if (previousAuthentication != null) { + if (currentAuthentication != null) { + attachEvent(AUTHENTICATION_REPLACED, toString(previousAuthentication, currentAuthentication)); + } + else { + attachEvent(AUTHENTICATION_CLEARED, toString(previousAuthentication)); + } + } + else if (currentAuthentication != null) { + attachEvent(AUTHENTICATION_SET, toString(currentAuthentication)); + } + // null-null is not handled since we won't create an event for that case + } + + private String toString(Authentication previousAuthentication, Authentication currentAuthentication) { + return toString(previousAuthentication) + " -> " + toString(currentAuthentication); + } + + private String toString(Authentication authentication) { + return authentication != null ? authentication.getClass().getSimpleName() + authentication.getAuthorities() + : "null"; + } + + private void attachEvent(SleuthSecurityEvent sleuthSecurityEvent, String... params) { + Span span = this.tracer.currentSpan(); + if (span != null) { + String event = format(sleuthSecurityEvent.getValue(), (Object[]) params); + LOGGER.info(event); + SECURITY_CONTEXT_CHANGE.wrap(span).event(event); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java new file mode 100644 index 000000000..28da983ba --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.instrument.security; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextChangedEvent; +import org.springframework.security.core.context.SecurityContextImpl; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * @author Jonatan Ivanov + */ +@ExtendWith(MockitoExtension.class) +class TracingSecurityContextChangedListenerTests { + + @Mock + private Tracer tracer; + + @Mock + private Span span; + + @Captor + private ArgumentCaptor eventCaptor; + + @InjectMocks + TracingSecurityContextChangedListener listener; + + @Test + void null_authentication_objects_should_result_in_noop() { + listener.securityContextChanged(new SecurityContextChangedEvent(null, null)); + listener.securityContextChanged(new SecurityContextChangedEvent(new SecurityContextImpl(null), null)); + listener.securityContextChanged(new SecurityContextChangedEvent(null, new SecurityContextImpl(null))); + listener.securityContextChanged( + new SecurityContextChangedEvent(new SecurityContextImpl(null), new SecurityContextImpl(null))); + + verifyNoInteractions(tracer); + } + + @Test + void null_span_should_result_in_noop() { + when(tracer.currentSpan()).thenReturn(null); + listener.securityContextChanged(new SecurityContextChangedEvent(null, fakeSecurityContext())); + + verify(tracer).currentSpan(); + } + + @Test + void set_context_changed_event_should_result_in_new_even_on_the_span() { + when(tracer.currentSpan()).thenReturn(span); + listener.securityContextChanged(new SecurityContextChangedEvent(null, fakeSecurityContext())); + + verify(tracer).currentSpan(); + verify(span).event(eventCaptor.capture()); + + then(eventCaptor.getValue()).isEqualTo("Authentication set AnonymousAuthenticationToken[ANONYMOUS]"); + } + + @Test + void clear_context_changed_event_should_result_in_new_even_on_the_span() { + when(tracer.currentSpan()).thenReturn(span); + listener.securityContextChanged(new SecurityContextChangedEvent(fakeSecurityContext(), null)); + + verify(tracer).currentSpan(); + verify(span).event(eventCaptor.capture()); + + then(eventCaptor.getValue()).isEqualTo("Authentication cleared AnonymousAuthenticationToken[ANONYMOUS]"); + } + + @Test + void replace_context_changed_event_should_result_in_new_even_on_the_span() { + when(tracer.currentSpan()).thenReturn(span); + listener.securityContextChanged(new SecurityContextChangedEvent(fakeSecurityContext(), fakeSecurityContext())); + + verify(tracer).currentSpan(); + verify(span).event(eventCaptor.capture()); + + then(eventCaptor.getValue()).isEqualTo( + "Authentication replaced AnonymousAuthenticationToken[ANONYMOUS] -> AnonymousAuthenticationToken[ANONYMOUS]"); + } + + private SecurityContext fakeSecurityContext() { + return new SecurityContextImpl(fakeAuthentication()); + } + + private Authentication fakeAuthentication() { + return new AnonymousAuthenticationToken("123", "username", + Collections.singletonList(new SimpleGrantedAuthority("ANONYMOUS"))); + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 8171631ef..04ffa5604 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -57,6 +57,7 @@ spring-cloud-sleuth-instrumentation-rxjava-tests spring-cloud-sleuth-instrumentation-r2dbc-tests spring-cloud-sleuth-instrumentation-scheduling-tests + spring-cloud-sleuth-instrumentation-security-tests spring-cloud-sleuth-instrumentation-task-tests spring-cloud-sleuth-instrumentation-webflux-tests spring-cloud-sleuth-instrumentation-rsocket-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml new file mode 100644 index 000000000..b8e760f20 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-security-tests + jar + Spring Cloud Sleuth Brave Security Instrumentation Tests + Spring Cloud Sleuth Brave Security Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + ${project.groupId} + spring-cloud-sleuth-tests-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java new file mode 100644 index 000000000..8cfc639db --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.brave.instrument.security; + +import brave.sampler.Sampler; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.instrument.security.SpringSecurityTests; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Jonatan Ivanov + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ContextConfiguration(classes = TracingSecurityContextChangedListenerIntegrationTests.Config.class) +public class TracingSecurityContextChangedListenerIntegrationTests extends SpringSecurityTests { + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + TestSpanHandler testSpanHandler(brave.test.TestSpanHandler spanHandler) { + return new BraveTestSpanHandler(spanHandler); + } + + @Bean + brave.test.TestSpanHandler spanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml new file mode 100644 index 000000000..5ec055055 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml @@ -0,0 +1,3 @@ +spring.application.name: spring-security-test-app + +#logging.level.org.springframework.security: TRACE diff --git a/tests/common/pom.xml b/tests/common/pom.xml index d6b158acd..a930ecd33 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -64,6 +64,11 @@ spring-integration-core true + + org.springframework.security + spring-security-core + true + org.springframework.boot spring-boot-starter-websocket diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java new file mode 100644 index 000000000..7071db5ac --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.instrument.security; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextChangedListener; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.springframework.cloud.sleuth.Span.Kind.SERVER; + +/** + * @author Jonatan Ivanov + */ +@ContextConfiguration(classes = SpringSecurityTests.Config.class) +public abstract class SpringSecurityTests { + + @Autowired + TestSpanHandler testSpanHandler; + + @Autowired + TestRestTemplate restTemplate; + + @BeforeEach + void setUp() { + testSpanHandler.clear(); + } + + @Test + void authenticated_user_should_trigger_events() { + long beforeStart = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); + ResponseEntity entity = restTemplate.withBasicAuth("user", "password").getForEntity("/", String.class); + long afterStop = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); + + then(entity.getStatusCode().is2xxSuccessful()).isTrue(); + then(entity.getBody()).isEqualTo("authenticated"); + + List> authEvents = getAuthEvents(testSpanHandler.reportedSpans()); + then(authEvents).isNotEmpty() + .allSatisfy(authEvent -> then(authEvent.getKey()).isStrictlyBetween(beforeStart, afterStop)); + + for (int i = 0; i < authEvents.size(); i += 2) { + String setEvent = authEvents.get(i).getValue(); + then(setEvent).isEqualTo("Authentication set UsernamePasswordAuthenticationToken[USER]"); + String clearEvent = authEvents.get(i + 1).getValue(); + then(clearEvent).isEqualTo("Authentication cleared UsernamePasswordAuthenticationToken[USER]"); + } + } + + @Test + void anonymous_should_trigger_events() { + long beforeStart = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); + ResponseEntity entity = restTemplate.getForEntity("/", String.class); + long afterStop = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); + + then(entity.getStatusCode().is2xxSuccessful()).isTrue(); + then(entity.getBody()).contains("html", "form"); + + List> authEvents = getAuthEvents(testSpanHandler.reportedSpans()); + then(authEvents).isNotEmpty() + .allSatisfy(authEvent -> then(authEvent.getKey()).isStrictlyBetween(beforeStart, afterStop)); + + for (int i = 0; i < authEvents.size(); i += 2) { + String setEvent = authEvents.get(i).getValue(); + then(setEvent).isEqualTo("Authentication set AnonymousAuthenticationToken[ROLE_ANONYMOUS]"); + String clearEvent = authEvents.get(i + 1).getValue(); + then(clearEvent).isEqualTo("Authentication cleared AnonymousAuthenticationToken[ROLE_ANONYMOUS]"); + } + } + + private List> getAuthEvents(List spans) { + return spans.stream().filter(span -> span.getKind() == SERVER).map(FinishedSpan::getEvents) + .flatMap(Collection::stream).filter(event -> event.getValue().contains("Authentication")) + .collect(Collectors.toList()); + } + + @EnableAutoConfiguration( + excludeName = "org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebClientAutoConfiguration") + @Configuration(proxyBeanMethods = false) + static class Config { + + // TODO: Remove this after Spring Boot auto-configuration is available + Config(List listeners) { + listeners.forEach(SecurityContextHolder::addListener); + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder().username("user") + .password("password").authorities("USER").build()); + } + + @Bean + HomeController homeController() { + return new HomeController(); + } + + } + + @RestController + static class HomeController { + + @GetMapping + String home() { + return "authenticated"; + } + + } + +}