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
This commit is contained in:
@@ -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.
|
||||
|
||||
21
pom.xml
21
pom.xml
@@ -76,6 +76,8 @@
|
||||
<brave.version>5.13.2</brave.version>
|
||||
<opentracing.version>0.32.0</opentracing.version>
|
||||
<spring-security-boot-autoconfigure.version>2.3.4.RELEASE</spring-security-boot-autoconfigure.version>
|
||||
<spring-security-oauth2.version>2.2.0.RELEASE</spring-security-oauth2.version>
|
||||
<spring-security-version>5.6.0-M2</spring-security-version>
|
||||
<disable.nohttp.checks>false</disable.nohttp.checks>
|
||||
<okhttp.version>4.9.0</okhttp.version>
|
||||
<mockwebserver.version>4.8.0</mockwebserver.version>
|
||||
@@ -83,7 +85,6 @@
|
||||
<javax.resource-api.version>1.7.1</javax.resource-api.version>
|
||||
<cglib-nodep.version>3.3.0</cglib-nodep.version>
|
||||
<objenesis.version>3.0.1</objenesis.version>
|
||||
<spring-security-oauth2.version>2.2.0.RELEASE</spring-security-oauth2.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<datasource-proxy.version>1.7</datasource-proxy.version>
|
||||
<tomcat-jdbc.version>10.0.6</tomcat-jdbc.version>
|
||||
@@ -267,11 +268,22 @@
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-bom</artifactId>
|
||||
<version>${spring-security-version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||
<version>${spring-security-boot-autoconfigure.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth</groupId>
|
||||
<artifactId>spring-security-oauth2</artifactId>
|
||||
<version>${spring-security-oauth2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cglib</groupId>
|
||||
@@ -289,11 +301,6 @@
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>${mockwebserver.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth</groupId>
|
||||
<artifactId>spring-security-oauth2</artifactId>
|
||||
<version>${spring-security-oauth2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.zipkin.aws</groupId>
|
||||
<artifactId>brave-propagation-aws</artifactId>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,\
|
||||
|
||||
@@ -212,6 +212,11 @@
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.kotlin</groupId>
|
||||
<artifactId>reactor-kotlin-extensions</artifactId>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> 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")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
<module>spring-cloud-sleuth-instrumentation-rxjava-tests</module>
|
||||
<module>spring-cloud-sleuth-instrumentation-r2dbc-tests</module>
|
||||
<module>spring-cloud-sleuth-instrumentation-scheduling-tests</module>
|
||||
<module>spring-cloud-sleuth-instrumentation-security-tests</module>
|
||||
<module>spring-cloud-sleuth-instrumentation-task-tests</module>
|
||||
<module>spring-cloud-sleuth-instrumentation-webflux-tests</module>
|
||||
<module>spring-cloud-sleuth-instrumentation-rsocket-tests</module>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
~
|
||||
~
|
||||
-->
|
||||
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-sleuth-instrumentation-security-tests</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Sleuth Brave Security Instrumentation Tests</name>
|
||||
<description>Spring Cloud Sleuth Brave Security Instrumentation Tests</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-sleuth-tests-brave</artifactId>
|
||||
<version>3.1.0-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<sonar.skip>true</sonar.skip>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<!--skip deploy -->
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-cloud-sleuth-tests-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-sleuth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.zipkin.brave</groupId>
|
||||
<artifactId>brave-tests</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
spring.application.name: spring-security-test-app
|
||||
|
||||
#logging.level.org.springframework.security: TRACE
|
||||
@@ -64,6 +64,11 @@
|
||||
<artifactId>spring-integration-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
|
||||
@@ -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<String> 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<Map.Entry<Long, String>> 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<String> entity = restTemplate.getForEntity("/", String.class);
|
||||
long afterStop = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
|
||||
|
||||
then(entity.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
then(entity.getBody()).contains("html", "form");
|
||||
|
||||
List<Map.Entry<Long, String>> 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<Map.Entry<Long, String>> getAuthEvents(List<FinishedSpan> 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<SecurityContextChangedListener> 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user