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:
Jonatan Ivanov
2021-08-25 15:44:45 -07:00
committed by GitHub
parent 1f986bb93f
commit f3051483d7
14 changed files with 672 additions and 7 deletions

View File

@@ -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
View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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,\

View File

@@ -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>

View File

@@ -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";
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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")));
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,3 @@
spring.application.name: spring-security-test-app
#logging.level.org.springframework.security: TRACE

View File

@@ -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>

View File

@@ -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";
}
}
}