diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java index 401518e7210d..eed05a4da462 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java @@ -136,7 +136,7 @@ private void captureRequestParameters(Span serverSpan, REQUEST request) { * created by servlet instrumentation we call this method on exit from the last servlet or filter. */ private void captureEnduserId(Span serverSpan, REQUEST request) { - if (!CommonConfig.get().shouldCaptureEnduser()) { + if (!CommonConfig.get().getEnduserConfig().isIdEnabled()) { return; } diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java index 8b68d8b3bb75..ce6eb514fb23 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java @@ -44,7 +44,7 @@ public void onEnd( ServletRequestContext requestContext, @Nullable ServletResponseContext responseContext, @Nullable Throwable error) { - if (CommonConfig.get().shouldCaptureEnduser()) { + if (CommonConfig.get().getEnduserConfig().isIdEnabled()) { Principal principal = accessor.getRequestUserPrincipal(requestContext.request()); if (principal != null) { String name = principal.getName(); diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/README.md b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md new file mode 100644 index 000000000000..892f5367d500 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md @@ -0,0 +1,14 @@ +# OpenTelemetry Javaagent Instrumentation: Spring Security Config + +Javaagent automatic instrumentation to capture `enduser.*` semantic attributes +from Spring Security `Authentication` objects. + +## Settings + +This module honors the [common `otel.instrumentation.common.enduser.*` properties](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#common-instrumentation-configuration) +and the following properties: + +| Property | Type | Default | Description | +|-------------------------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.spring-security.enduser.role.granted-authority-prefix` | String | `ROLE_` | Prefix of granted authorities identifying roles to capture in the `enduser.role` semantic attribute. | +| `otel.instrumentation.spring-security.enduser.scope.granted-authority-prefix` | String | `SCOPE_` | Prefix of granted authorities identifying scopes to capture in the `enduser.scopes` semantic attribute. | diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..f30b3eaad1f3 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.springframework.security") + module.set("spring-security-config") + versions.set("[6.0.0,]") + + extraDependency("jakarta.servlet:jakarta.servlet-api:6.0.0") + extraDependency("org.springframework.security:spring-security-web:6.0.0") + extraDependency("io.projectreactor:reactor-core:3.5.0") + } +} + +dependencies { + implementation(project(":instrumentation:spring:spring-security-config-6.0:library")) + + library("org.springframework.security:spring-security-config:6.0.0") + library("org.springframework.security:spring-security-web:6.0.0") + library("io.projectreactor:reactor-core:3.5.0") + + testLibrary("org.springframework:spring-test:6.0.0") + testLibrary("jakarta.servlet:jakarta.servlet-api:6.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks { + test { + systemProperty("otel.instrumentation.common.enduser.id.enabled", "true") + systemProperty("otel.instrumentation.common.enduser.role.enabled", "true") + systemProperty("otel.instrumentation.common.enduser.scope.enabled", "true") + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java new file mode 100644 index 000000000000..819e8a41ae7a --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; + +public class EnduserAttributesCapturerSingletons { + + private static final EnduserAttributesCapturer ENDUSER_ATTRIBUTES_CAPTURER = + createEndUserAttributesCapturerFromConfig(); + + private EnduserAttributesCapturerSingletons() {} + + public static EnduserAttributesCapturer enduserAttributesCapturer() { + return ENDUSER_ATTRIBUTES_CAPTURER; + } + + private static EnduserAttributesCapturer createEndUserAttributesCapturerFromConfig() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(CommonConfig.get().getEnduserConfig().isIdEnabled()); + capturer.setEnduserRoleEnabled(CommonConfig.get().getEnduserConfig().isRoleEnabled()); + capturer.setEnduserScopeEnabled(CommonConfig.get().getEnduserConfig().isScopeEnabled()); + + String rolePrefix = + InstrumentationConfig.get() + .getString( + "otel.instrumentation.spring-security.enduser.role.granted-authority-prefix"); + if (rolePrefix != null) { + capturer.setRoleGrantedAuthorityPrefix(rolePrefix); + } + + String scopePrefix = + InstrumentationConfig.get() + .getString( + "otel.instrumentation.spring-security.enduser.scope.granted-authority-prefix"); + if (scopePrefix != null) { + capturer.setScopeGrantedAuthorityPrefix(rolePrefix); + } + return capturer; + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java new file mode 100644 index 000000000000..a4153cceafd6 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet; + +import static io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerSingletons.enduserAttributesCapturer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet.EnduserAttributesHttpSecurityCustomizer; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +/** Instrumentation for {@link HttpSecurity}. */ +public class HttpSecurityInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.security.config.annotation.web.builders.HttpSecurity"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isProtected()).and(named("performBuild")).and(takesArguments(0)), + getClass().getName() + "$PerformBuildAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformBuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This HttpSecurity httpSecurity) { + new EnduserAttributesHttpSecurityCustomizer(enduserAttributesCapturer()) + .customize(httpSecurity); + } + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java new file mode 100644 index 000000000000..6e87a1ba17ca --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +/** Instrumentation module for servlet-based applications that use spring-security-config. */ +@AutoService(InstrumentationModule.class) +public class SpringSecurityConfigServletInstrumentationModule extends InstrumentationModule { + public SpringSecurityConfigServletInstrumentationModule() { + super("spring-security-config-servlet", "spring-security-config-servlet-6.0"); + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return super.defaultEnabled(config) + /* + * Since the only thing this module currently does is capture enduser attributes, + * the module can be completely disabled if enduser attributes are disabled. + * + * If any functionality not related to enduser attributes is added to this module, + * then this check will need to move elsewhere to only guard the enduser attributes logic. + */ + && CommonConfig.get().getEnduserConfig().isAnyEnabled(); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + /* + * Ensure this module is only applied to Spring Security >= 6.0, + * since Spring Security >= 6.0 uses Jakarta EE rather than Java EE, + * and this instrumentation module uses Jakarta EE. + */ + return hasClassesNamed( + "org.springframework.security.authentication.ObservationAuthenticationManager"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpSecurityInstrumentation()); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java new file mode 100644 index 000000000000..d75f3103f18d --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux; + +import static io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerSingletons.enduserAttributesCapturer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux.EnduserAttributesServerHttpSecurityCustomizer; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.security.config.web.server.ServerHttpSecurity; + +/** Instrumentation for {@link ServerHttpSecurity}. */ +public class ServerHttpSecurityInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.security.config.web.server.ServerHttpSecurity"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This ServerHttpSecurity serverHttpSecurity) { + new EnduserAttributesServerHttpSecurityCustomizer(enduserAttributesCapturer()) + .customize(serverHttpSecurity); + } + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java new file mode 100644 index 000000000000..a219a7eccbd3 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; + +/** Instrumentation module for webflux-based applications that use spring-security-config. */ +@AutoService(InstrumentationModule.class) +public class SpringSecurityConfigWebFluxInstrumentationModule extends InstrumentationModule { + + public SpringSecurityConfigWebFluxInstrumentationModule() { + super("spring-security-config-webflux", "spring-security-config-webflux-6.0"); + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return super.defaultEnabled(config) + /* + * Since the only thing this module currently does is capture enduser attributes, + * the module can be completely disabled if enduser attributes are disabled. + * + * If any functionality not related to enduser attributes is added to this module, + * then this check will need to move elsewhere to only guard the enduser attributes logic. + */ + && CommonConfig.get().getEnduserConfig().isAnyEnabled(); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ServerHttpSecurityInstrumentation()); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java new file mode 100644 index 000000000000..df936006c59c --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet.EnduserAttributesCapturingServletFilter; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(SpringExtension.class) +class HttpSecurityInstrumentationTest { + + @Configuration + static class TestConfiguration {} + + @Mock ObjectPostProcessor objectPostProcessor; + + /** + * Ensures that {@link HttpSecurityInstrumentation} registers a {@link + * EnduserAttributesCapturingServletFilter} in the filter chain. + * + *

Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered(@Autowired ApplicationContext applicationContext) throws Exception { + + AuthenticationManagerBuilder authenticationBuilder = + new AuthenticationManagerBuilder(objectPostProcessor); + + HttpSecurity httpSecurity = + new HttpSecurity( + objectPostProcessor, + authenticationBuilder, + Collections.singletonMap(ApplicationContext.class, applicationContext)); + + DefaultSecurityFilterChain filterChain = httpSecurity.build(); + + assertThat(filterChain.getFilters()) + .filteredOn( + item -> + item.getClass() + .getName() + .endsWith(EnduserAttributesCapturingServletFilter.class.getSimpleName())) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java new file mode 100644 index 000000000000..7ad60fd21f5a --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux.EnduserAttributesCapturingWebFilter; +import org.junit.jupiter.api.Test; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +class ServerHttpSecurityInstrumentationTest { + + /** + * Ensures that {@link ServerHttpSecurityInstrumentation} registers a {@link + * EnduserAttributesCapturingWebFilter} in the filter chain. + * + *

Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered() { + + ServerHttpSecurity serverHttpSecurity = ServerHttpSecurity.http(); + + SecurityWebFilterChain securityWebFilterChain = serverHttpSecurity.build(); + + assertThat(securityWebFilterChain.getWebFilters().collectList().block()) + .filteredOn( + item -> + item.getClass() + .getName() + .endsWith(EnduserAttributesCapturingWebFilter.class.getSimpleName())) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/README.md b/instrumentation/spring/spring-security-config-6.0/library/README.md new file mode 100644 index 000000000000..ab735c5586b1 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/README.md @@ -0,0 +1,77 @@ +# OpenTelemetry Instrumentation: Spring Security Config + +Provides a Servlet `Filter` and a WebFlux `WebFilter` to capture `enduser.*` semantic attributes +from Spring Security `Authentication` objects. + +Also provides `Customizer` implementations to insert those filters into the filter chains created by +`HttpSecurity` and `ServerHttpSecurity`, respectively. + +## Usage in Spring WebMVC Applications + +When not using [automatic instrumentation](../javaagent/), you can enable enduser attribute capturing +for a `SecurityFilterChain` by appling an `EnduserAttributesHttpSecurityCustomizer` +to the `HttpSecurity` which constructs the `SecurityFilterChain`. + +```java +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet.EnduserAttributesHttpSecurityCustomizer; + +@Configuration +@EnableWebSecurity +class MyWebSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // First, apply application related configuration to http + + // Then, apply enduser.* attribute capturing + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + // Set properties of capturer. Defaults shown. + capturer.setEnduserIdEnabled(false); + capturer.setEnduserRoleEnabled(false); + capturer.setEnduserScopeEnabled(false); + capturer.setRoleGrantedAuthorityPrefix("ROLE_"); + capturer.setScopeGrantedAuthorityPrefix("SCOPE_"); + + new EnduserAttributesHttpSecurityCustomizer(capturer) + .customize(http); + + return http.build(); + } +} +``` + +## Usage in Spring WebFlux Applications + +When not using [automatic instrumentation](../javaagent/), you can enable enduser attribute capturing +for a `SecurityWebFilterChain` by appling an `EnduserAttributesServerHttpSecurityCustomizer` +to the `ServerHttpSecurity` which constructs the `SecurityWebFilterChain`. + +```java +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux.EnduserAttributesServerHttpSecurityCustomizer; + +@Configuration +@EnableWebFluxSecurity +class MyWebFluxSecurityConfig { + + @Bean + public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception { + // First, apply application related configuration to http + + // Then, apply enduser.* attribute capturing + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + // Set properties of capturer. Defaults shown. + capturer.setEnduserIdEnabled(false); + capturer.setEnduserRoleEnabled(false); + capturer.setEnduserScopeEnabled(false); + capturer.setRoleGrantedAuthorityPrefix("ROLE_"); + capturer.setScopeGrantedAuthorityPrefix("SCOPE_"); + + new EnduserAttributesServerHttpSecurityCustomizer(capturer) + .customize(http); + + return http.build(); + } +} +``` diff --git a/instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts b/instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts new file mode 100644 index 000000000000..12f62f98fe1e --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("org.springframework.security:spring-security-config:6.0.0") + library("org.springframework.security:spring-security-web:6.0.0") + library("org.springframework:spring-web:6.0.0") + library("io.projectreactor:reactor-core:3.5.0") + library("jakarta.servlet:jakarta.servlet-api:6.0.0") + + implementation(project(":instrumentation:reactor:reactor-3.1:library")) + + testImplementation(project(":testing-common")) + testImplementation("org.springframework:spring-test:6.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java new file mode 100644 index 000000000000..7e7ab3ef0af9 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.semconv.SemanticAttributes; +import java.util.Objects; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * Captures {@code enduser.*} semantic attributes from {@link Authentication} objects. + * + *

After construction, you must selectively enable which attributes you want captured by calling + * the appropriate {@code setEnduser*Enabled(true)} method. + */ +public final class EnduserAttributesCapturer { + + private static final String DEFAULT_ROLE_PREFIX = "ROLE_"; + private static final String DEFAULT_SCOPE_PREFIX = "SCOPE_"; + + /** Determines if the {@code enduser.id} attribute should be captured. */ + private boolean enduserIdEnabled; + + /** Determines if the {@code enduser.role} attribute should be captured. */ + private boolean enduserRoleEnabled; + + /** Determines if the {@code enduser.scope} attribute should be captured. */ + private boolean enduserScopeEnabled; + + /** The prefix used to find {@link GrantedAuthority} objects for roles. */ + private String roleGrantedAuthorityPrefix = DEFAULT_ROLE_PREFIX; + + /** The prefix used to find {@link GrantedAuthority} objects for scopes. */ + private String scopeGrantedAuthorityPrefix = DEFAULT_SCOPE_PREFIX; + + /** + * Captures the {@code enduser.*} semantic attributes from the given {@link Authentication} into + * the {@link LocalRootSpan} of the given {@link Context}. + * + *

Only the attributes enabled via the {@code setEnduser*Enabled(true)} methods are captured. + * + *

The following attributes can be captured: + * + *

    + *
  • {@code enduser.id} - from {@link Authentication#getName()} + *
  • {@code enduser.role} - a comma-separated list from the {@link + * Authentication#getAuthorities()} with the configured {@link + * #getRoleGrantedAuthorityPrefix() role prefix} + *
  • {@code enduser.scope} - a comma-separated list from the {@link + * Authentication#getAuthorities()} with the configured {@link + * #getScopeGrantedAuthorityPrefix() scope prefix} + *
+ * + * @param otelContext the context from which the {@link LocalRootSpan} in which to capture the + * attributes will be retrieved + * @param authentication the authentication from which to determine the {@code enduser.*} + * attributes. + */ + public void captureEnduserAttributes(Context otelContext, Authentication authentication) { + if (authentication != null) { + Span localRootSpan = LocalRootSpan.fromContext(otelContext); + + if (enduserIdEnabled && authentication.getName() != null) { + localRootSpan.setAttribute(SemanticAttributes.ENDUSER_ID, authentication.getName()); + } + + StringBuilder roleBuilder = null; + StringBuilder scopeBuilder = null; + if (enduserRoleEnabled || enduserScopeEnabled) { + for (GrantedAuthority authority : authentication.getAuthorities()) { + String authorityString = authority.getAuthority(); + if (enduserRoleEnabled && authorityString.startsWith(roleGrantedAuthorityPrefix)) { + roleBuilder = appendSuffix(roleGrantedAuthorityPrefix, authorityString, roleBuilder); + } else if (enduserScopeEnabled + && authorityString.startsWith(scopeGrantedAuthorityPrefix)) { + scopeBuilder = appendSuffix(scopeGrantedAuthorityPrefix, authorityString, scopeBuilder); + } + } + } + if (roleBuilder != null) { + localRootSpan.setAttribute(SemanticAttributes.ENDUSER_ROLE, roleBuilder.toString()); + } + if (scopeBuilder != null) { + localRootSpan.setAttribute(SemanticAttributes.ENDUSER_SCOPE, scopeBuilder.toString()); + } + } + } + + private static StringBuilder appendSuffix( + String prefix, String authorityString, StringBuilder builder) { + if (authorityString.length() > prefix.length()) { + String suffix = authorityString.substring(prefix.length()); + if (builder == null) { + builder = new StringBuilder(); + builder.append(suffix); + } else { + builder.append(",").append(suffix); + } + } + return builder; + } + + public boolean isEnduserIdEnabled() { + return enduserIdEnabled; + } + + public void setEnduserIdEnabled(boolean enduserIdEnabled) { + this.enduserIdEnabled = enduserIdEnabled; + } + + public boolean isEnduserRoleEnabled() { + return enduserRoleEnabled; + } + + public void setEnduserRoleEnabled(boolean enduserRoleEnabled) { + this.enduserRoleEnabled = enduserRoleEnabled; + } + + public boolean isEnduserScopeEnabled() { + return enduserScopeEnabled; + } + + public void setEnduserScopeEnabled(boolean enduserScopeEnabled) { + this.enduserScopeEnabled = enduserScopeEnabled; + } + + public String getRoleGrantedAuthorityPrefix() { + return roleGrantedAuthorityPrefix; + } + + public void setRoleGrantedAuthorityPrefix(String roleGrantedAuthorityPrefix) { + this.roleGrantedAuthorityPrefix = + Objects.requireNonNull(roleGrantedAuthorityPrefix, "rolePrefix must not be null"); + } + + public String getScopeGrantedAuthorityPrefix() { + return scopeGrantedAuthorityPrefix; + } + + public void setScopeGrantedAuthorityPrefix(String scopeGrantedAuthorityPrefix) { + this.scopeGrantedAuthorityPrefix = + Objects.requireNonNull(scopeGrantedAuthorityPrefix, "scopePrefix must not be null"); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java new file mode 100644 index 000000000000..c159576b6577 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import java.util.Objects; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * A servlet {@link Filter} that captures {@code endpoint.*} semantic attributes from the {@link + * org.springframework.security.core.Authentication} in the current {@link + * org.springframework.security.core.context.SecurityContext} retrieved from {@link + * SecurityContextHolder}. + * + *

Inserted into the filter chain by {@link EnduserAttributesHttpSecurityCustomizer} after all + * the filters that populate the {@link org.springframework.security.core.context.SecurityContext} + * in the {@link org.springframework.security.core.context.SecurityContextHolder}. + */ +public class EnduserAttributesCapturingServletFilter implements Filter { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesCapturingServletFilter(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + capturer.captureEnduserAttributes( + Context.current(), SecurityContextHolder.getContext().getAuthentication()); + + chain.doFilter(request, response); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java new file mode 100644 index 000000000000..7480aed192ce --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Objects; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.access.intercept.AuthorizationFilter; + +/** + * Customizes a {@link HttpSecurity} by inserting a {@link EnduserAttributesCapturingServletFilter} + * after all the filters that populate the {@link + * org.springframework.security.core.context.SecurityContext} in the {@link + * org.springframework.security.core.context.SecurityContextHolder}. + */ +public class EnduserAttributesHttpSecurityCustomizer implements Customizer { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesHttpSecurityCustomizer(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public void customize(HttpSecurity httpSecurity) { + /* + * See org.springframework.security.config.annotation.web.builders.FilterOrderRegistration + * for where this appears in the chain. + */ + httpSecurity.addFilterBefore( + new EnduserAttributesCapturingServletFilter(capturer), AuthorizationFilter.class); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java new file mode 100644 index 000000000000..0b6c8533b698 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Objects; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * A {@link WebFilter} that captures {@code endpoint.*} semantic attributes from the {@link + * org.springframework.security.core.Authentication} in the current {@link + * org.springframework.security.core.context.SecurityContext} retrieved from {@link + * ReactiveSecurityContextHolder}. + * + *

Inserted into the filter chain by {@link EnduserAttributesServerHttpSecurityCustomizer} after + * all the filters that populate the {@link + * org.springframework.security.core.context.SecurityContext} in the {@link + * org.springframework.security.core.context.ReactiveSecurityContextHolder}. + */ +public class EnduserAttributesCapturingWebFilter implements WebFilter { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesCapturingWebFilter(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + Context threadLocalOtelContext = Context.current(); + + return Mono.zip(ReactiveSecurityContextHolder.getContext(), Mono.deferContextual(Mono::just)) + .doOnNext( + t2 -> + capturer.captureEnduserAttributes( + ContextPropagationOperator.getOpenTelemetryContext( + reactor.util.context.Context.of(t2.getT2()), threadLocalOtelContext), + t2.getT1().getAuthentication())) + .then(chain.filter(exchange)); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java new file mode 100644 index 000000000000..06e036100579 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Objects; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; + +/** + * Customizes a {@link ServerHttpSecurity} by inserting a {@link + * EnduserAttributesCapturingWebFilter} after all the filters that populate the {@link + * org.springframework.security.core.context.SecurityContext} in the {@link + * org.springframework.security.core.context.ReactiveSecurityContextHolder}. + */ +public class EnduserAttributesServerHttpSecurityCustomizer + implements Customizer { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesServerHttpSecurityCustomizer(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public void customize(ServerHttpSecurity serverHttpSecurity) { + serverHttpSecurity.addFilterBefore( + new EnduserAttributesCapturingWebFilter(capturer), SecurityWebFiltersOrder.LOGOUT); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java new file mode 100644 index 000000000000..42720f9a3834 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java @@ -0,0 +1,217 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.SemanticAttributes; +import java.util.Arrays; +import java.util.function.Consumer; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +public class EnduserAttributesCapturerTest { + + @RegisterExtension InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void nothingEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(SemanticAttributes.ENDUSER_ID)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); + } + + @Test + void allEnabledButNoRoles() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2")); + } + + @Test + void allEnabledButNoScopes() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); + } + + @Test + void onlyEnduserIdEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); + } + + @Test + void onlyEnduserRoleEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserRoleEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(SemanticAttributes.ENDUSER_ID)) + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); + } + + @Test + void onlyEnduserScopeEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserScopeEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(SemanticAttributes.ENDUSER_ID)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2")); + } + + @Test + void allEnabledAndAlternatePrefix() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + capturer.setRoleGrantedAuthorityPrefix("role_"); + capturer.setScopeGrantedAuthorityPrefix("scope_"); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("role_role1"), + new SimpleGrantedAuthority("role_role2"), + new SimpleGrantedAuthority("scope_scope1"), + new SimpleGrantedAuthority("scope_scope2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2")); + } + + void test( + EnduserAttributesCapturer capturer, + Authentication authentication, + Consumer assertions) { + testing.runWithHttpServerSpan( + () -> { + Context otelContext = Context.current(); + capturer.captureEnduserAttributes(otelContext, authentication); + }); + + testing.waitAndAssertTraces(trace -> trace.hasSpansSatisfyingExactly(assertions)); + } + + private static Condition attribute(AttributeKey attributeKey) { + return new Condition<>( + spanData -> spanData.getAttributes().get(attributeKey) != null, + "attribute " + attributeKey); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java new file mode 100644 index 000000000000..792764047412 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.SemanticAttributes; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +class EnduserAttributesCapturingServletFilterTest { + + @RegisterExtension InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + /** + * Tests to ensure enduser attributes are captured. + * + *

This just tests one scenario of {@link EnduserAttributesCapturer} to ensure that it is + * invoked properly by the filter. {@link + * io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerTest} + * tests many other scenarios. + */ + @Test + void test() throws Exception { + + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + EnduserAttributesCapturingServletFilter filter = + new EnduserAttributesCapturingServletFilter(capturer); + + testing.runWithHttpServerSpan( + () -> { + ServletRequest request = new MockHttpServletRequest(); + ServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = new MockFilterChain(); + + SecurityContext previousSecurityContext = SecurityContextHolder.getContext(); + try { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication( + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2")))); + SecurityContextHolder.setContext(securityContext); + + filter.doFilter(request, response, filterChain); + } finally { + SecurityContextHolder.setContext(previousSecurityContext); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2"))); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java new file mode 100644 index 000000000000..d96c14653649 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(SpringExtension.class) +class EnduserAttributesHttpSecurityCustomizerTest { + + @Configuration + static class TestConfiguration {} + + @Mock ObjectPostProcessor objectPostProcessor; + + /** + * Ensures that the {@link EnduserAttributesHttpSecurityCustomizer} registers a {@link + * EnduserAttributesCapturingServletFilter} in the filter chain. + * + *

Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered(@Autowired ApplicationContext applicationContext) throws Exception { + + AuthenticationManagerBuilder authenticationBuilder = + new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity httpSecurity = + new HttpSecurity( + objectPostProcessor, + authenticationBuilder, + Collections.singletonMap(ApplicationContext.class, applicationContext)); + + EnduserAttributesHttpSecurityCustomizer customizer = + new EnduserAttributesHttpSecurityCustomizer(new EnduserAttributesCapturer()); + customizer.customize(httpSecurity); + + DefaultSecurityFilterChain filterChain = httpSecurity.build(); + + assertThat(filterChain.getFilters()) + .filteredOn(EnduserAttributesCapturingServletFilter.class::isInstance) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java new file mode 100644 index 000000000000..1db46c41c6e2 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.SemanticAttributes; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.server.handler.DefaultWebFilterChain; +import reactor.core.publisher.Mono; + +class EnduserAttributesCapturingWebFilterTest { + + @RegisterExtension InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + /** + * Tests to ensure enduser attributes are captured. + * + *

This just tests one scenario of {@link EnduserAttributesCapturer} to ensure that it is + * invoked properly by the filter. {@link + * io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerTest} + * tests many other scenarios. + */ + @Test + void test() { + + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + EnduserAttributesCapturingWebFilter filter = new EnduserAttributesCapturingWebFilter(capturer); + + testing.runWithHttpServerSpan( + () -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + DefaultWebFilterChain filterChain = + new DefaultWebFilterChain(exch -> Mono.empty(), Collections.emptyList()); + Context otelContext = Context.current(); + filter + .filter(exchange, filterChain) + .contextWrite( + ReactiveSecurityContextHolder.withAuthentication( + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))))) + .contextWrite( + context -> + ContextPropagationOperator.storeOpenTelemetryContext(context, otelContext)) + .block(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2"))); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java new file mode 100644 index 000000000000..17fb42675f05 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import org.junit.jupiter.api.Test; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +class EnduserAttributesServerHttpSecurityCustomizerTest { + + /** + * Ensures that the {@link EnduserAttributesServerHttpSecurityCustomizer} registers a {@link + * EnduserAttributesCapturingWebFilter} in the filter chain. + * + *

Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered() { + + ServerHttpSecurity serverHttpSecurity = ServerHttpSecurity.http(); + + EnduserAttributesServerHttpSecurityCustomizer customizer = + new EnduserAttributesServerHttpSecurityCustomizer(new EnduserAttributesCapturer()); + + customizer.customize(serverHttpSecurity); + + SecurityWebFilterChain securityWebFilterChain = serverHttpSecurity.build(); + + assertThat(securityWebFilterChain.getWebFilters().collectList().block()) + .filteredOn(EnduserAttributesCapturingWebFilter.class::isInstance) + .hasSize(1); + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java index a18cab65e201..4d03306bb537 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java @@ -32,10 +32,10 @@ public static CommonConfig get() { private final List serverRequestHeaders; private final List serverResponseHeaders; private final Set knownHttpRequestMethods; + private final EnduserConfig enduserConfig; private final boolean statementSanitizationEnabled; private final boolean emitExperimentalHttpClientTelemetry; private final boolean emitExperimentalHttpServerTelemetry; - private final boolean captureEnduser; CommonConfig(InstrumentationConfig config) { peerServiceResolver = @@ -82,7 +82,7 @@ public static CommonConfig get() { "otel.instrumentation.http.server.emit-experimental-metrics", "otel.instrumentation.http.server.emit-experimental-telemetry", false); - captureEnduser = config.getBoolean("otel.instrumentation.common.enduser.id.enabled", false); + enduserConfig = new EnduserConfig(config); } public PeerServiceResolver getPeerServiceResolver() { @@ -109,6 +109,10 @@ public Set getKnownHttpRequestMethods() { return knownHttpRequestMethods; } + public EnduserConfig getEnduserConfig() { + return enduserConfig; + } + public boolean isStatementSanitizationEnabled() { return statementSanitizationEnabled; } @@ -120,8 +124,4 @@ public boolean shouldEmitExperimentalHttpClientTelemetry() { public boolean shouldEmitExperimentalHttpServerTelemetry() { return emitExperimentalHttpServerTelemetry; } - - public boolean shouldCaptureEnduser() { - return captureEnduser; - } } diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EnduserConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EnduserConfig.java new file mode 100644 index 000000000000..fbde1c90aaa2 --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EnduserConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.internal; + +import java.util.Objects; + +/** + * Configuration that controls capturing the {@code enduser.*} semantic attributes. + * + *

The {@code enduser.*} semantic attributes are not captured by default, due to this text in the + * specification: + * + *

+ * + * Given the sensitive nature of this information, SDKs and exporters SHOULD drop these attributes + * by default and then provide a configuration parameter to turn on retention for use cases where + * the information is required and would not violate any policies or regulations. + * + *
+ * + *

Capturing of the {@code enduser.*} semantic attributes can be individually enabled by + * configured the following properties: + * + *

+ * otel.instrumentation.common.enduser.id.enabled=true
+ * otel.instrumentation.common.enduser.role.enabled=true
+ * otel.instrumentation.common.enduser.scope.enabled=true
+ * 
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class EnduserConfig { + + private final boolean idEnabled; + private final boolean roleEnabled; + private final boolean scopeEnabled; + + EnduserConfig(InstrumentationConfig instrumentationConfig) { + Objects.requireNonNull(instrumentationConfig, "instrumentationConfig must not be null"); + + /* + * Capturing enduser.* attributes is disabled by default, because of this requirement in the specification: + * + * Given the sensitive nature of this information, SDKs and exporters SHOULD drop these attributes by default and then provide a configuration parameter to turn on retention for use cases where the information is required and would not violate any policies or regulations. + * + * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#general-identity-attributes + */ + this.idEnabled = + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.id.enabled", false); + this.roleEnabled = + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.role.enabled", false); + this.scopeEnabled = + instrumentationConfig.getBoolean( + "otel.instrumentation.common.enduser.scope.enabled", false); + } + + /** + * Returns true if capturing of any {@code enduser.*} semantic attribute is enabled. + * + *

This flag can be used by capturing instrumentations to bypass all {@code enduser.*} + * attribute capturing. + */ + public boolean isAnyEnabled() { + return this.idEnabled || this.roleEnabled || this.scopeEnabled; + } + + /** + * Returns true if capturing the {@code enduser.id} semantic attribute is enabled. + * + * @return true if capturing the {@code enduser.id} semantic attribute is enabled. + */ + public boolean isIdEnabled() { + return this.idEnabled; + } + + /** + * Returns true if capturing the {@code enduser.role} semantic attribute is enabled. + * + * @return true if capturing the {@code enduser.role} semantic attribute is enabled. + */ + public boolean isRoleEnabled() { + return this.roleEnabled; + } + + /** + * Returns true if capturing the {@code enduser.scope} semantic attribute is enabled. + * + * @return true if capturing the {@code enduser.scope} semantic attribute is enabled. + */ + public boolean isScopeEnabled() { + return this.scopeEnabled; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index caa039da5706..d63927f70ff6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -513,6 +513,8 @@ include(":instrumentation:spring:spring-rabbit-1.0:javaagent") include(":instrumentation:spring:spring-rmi-4.0:javaagent") include(":instrumentation:spring:spring-scheduling-3.1:bootstrap") include(":instrumentation:spring:spring-scheduling-3.1:javaagent") +include(":instrumentation:spring:spring-security-config-6.0:javaagent") +include(":instrumentation:spring:spring-security-config-6.0:library") include(":instrumentation:spring:spring-web:spring-web-3.1:javaagent") include(":instrumentation:spring:spring-web:spring-web-3.1:library") include(":instrumentation:spring:spring-web:spring-web-3.1:testing")