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..83d5fa91e1df --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md @@ -0,0 +1,15 @@ +# OpenTelemetry Javaagent Instrumentation: Spring Security Config + +Javaagent automatic instrumentation to capture `enduser.*` semantic attributes +from Spring Security `Authentication` objects. + +## Settings + +| Property | Type | Default | Description | +|-------------------------------------------------------------------------------|---------|----------|-------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.common.enduser.enabled` | Boolean | `false` | Whether to capture `enduser.*` semantic attributes. Must be set to true to capture any `enduser.*` attributes. | +| `otel.instrumentation.common.enduser.id.enabled` | Boolean | `true` | Whether to capture `enduser.id` semantic attribute. Only takes effect if `otel.instrumentation.common.enduser.enabled` is true. | +| `otel.instrumentation.common.enduser.role.enabled` | Boolean | `true` | Whether to capture `enduser.role` semantic attribute. Only takes effect if `otel.instrumentation.common.enduser.enabled` is true. | +| `otel.instrumentation.common.enduser.scope.enabled` | Boolean | `true` | Whether to capture `enduser.scope` semantic attribute. Only takes effect if `otel.instrumentation.common.enduser.enabled` is true. | +| `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..19f0cbc99b3b --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts @@ -0,0 +1,39 @@ +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 { + bootstrap(project(":instrumentation:executors:bootstrap")) + + implementation(project(":instrumentation:spring:spring-security-config-6.0:library")) + implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + + 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") + + testImplementation("org.springframework:spring-test:6.0.0") + testImplementation("jakarta.servlet:jakarta.servlet-api:6.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks { + test { + systemProperty("otel.instrumentation.common.enduser.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..36b7e202b4bc --- /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,52 @@ +/* + * 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) { + /* + * 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. + */ + return CommonConfig.get().getEnduserConfig().isEnabled(); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "org.springframework.security.config.annotation.web.builders.HttpSecurity") + .and( + hasClassesNamed( + "org.springframework.security.web.access.intercept.AuthorizationFilter")) + .and(hasClassesNamed("jakarta.servlet.Servlet")); + } + + @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..94f2b8206a58 --- /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,48 @@ +/* + * 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.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 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) { + /* + * 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. + */ + return CommonConfig.get().getEnduserConfig().isEnabled(); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.springframework.security.config.web.server.ServerHttpSecurity"); + } + + @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..700342bb605e --- /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(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + 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(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + 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..b018ffb5635b --- /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,122 @@ +/* + * 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. */ +public 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 = true; + + /** Determines if the {@code enduser.role} attribute should be captured. */ + private boolean enduserRoleEnabled = true; + + /** Determines if the {@code enduser.scope} attribute should be captured. */ + private boolean enduserScopeEnabled = true; + + /** 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; + + 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..b0e0019d36f1 --- /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,226 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.SemanticAttributes; +import java.util.Arrays; +import java.util.List; +import org.assertj.core.api.ThrowingConsumer; +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 defaults() { + 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"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("scope1,scope2"); + }; + + test(capturer, authentication, assertions); + } + + @Test + void noRoles() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("scope1,scope2"); + }; + + test(capturer, authentication, assertions); + } + + @Test + void noScopes() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)).isNull(); + }; + + test(capturer, authentication, assertions); + } + + @Test + void disableEnduserId() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(false); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("scope1,scope2"); + }; + + test(capturer, authentication, assertions); + } + + @Test + void disableEnduserRole() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserRoleEnabled(false); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("scope1,scope2"); + }; + + test(capturer, authentication, assertions); + } + + @Test + void disableEnduserScope() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserScopeEnabled(false); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)).isNull(); + }; + + test(capturer, authentication, assertions); + } + + @Test + void alternatePrefix() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + 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"))); + + ThrowingConsumer assertions = + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("scope1,scope2"); + }; + + test(capturer, authentication, assertions); + } + + void test( + EnduserAttributesCapturer capturer, + Authentication authentication, + ThrowingConsumer assertions) { + testing.runWithHttpServerSpan( + () -> { + Context otelContext = Context.current(); + capturer.captureEnduserAttributes(otelContext, authentication); + }); + + List spans = testing.spans(); + assertThat(spans).singleElement().satisfies(assertions); + } +} 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..2451948e5785 --- /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,88 @@ +/* + * 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 io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.SemanticAttributes; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.util.Arrays; +import java.util.List; +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(); + 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); + } + }); + + List spans = testing.spans(); + assertThat(spans) + .singleElement() + .satisfies( + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("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..394fb4fe0e08 --- /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,86 @@ +/* + * 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.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.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.SemanticAttributes; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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(); + 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(); + }); + + List spans = testing.spans(); + assertThat(spans) + .singleElement() + .satisfies( + span -> { + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)) + .isEqualTo("role1,role2"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("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 5d4451086286..cbcdbc2eee99 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 emitExperimentalHttpClientMetrics; private final boolean emitExperimentalHttpServerMetrics; - private final boolean captureEnduser; CommonConfig(InstrumentationConfig config) { peerServiceResolver = @@ -74,8 +74,8 @@ public static CommonConfig get() { config.getBoolean("otel.instrumentation.http.client.emit-experimental-metrics", false); emitExperimentalHttpServerMetrics = config.getBoolean("otel.instrumentation.http.server.emit-experimental-metrics", false); - captureEnduser = - config.getBoolean("otel.instrumentation.common.capture-enduser.enabled", false); + + enduserConfig = new EnduserConfig(config); } public PeerServiceResolver getPeerServiceResolver() { @@ -102,6 +102,10 @@ public Set getKnownHttpRequestMethods() { return knownHttpRequestMethods; } + public EnduserConfig getEnduserConfig() { + return enduserConfig; + } + public boolean isStatementSanitizationEnabled() { return statementSanitizationEnabled; } @@ -114,7 +118,4 @@ public boolean shouldEmitExperimentalHttpServerMetrics() { return emitExperimentalHttpServerMetrics; } - 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..e1db321a560c --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EnduserConfig.java @@ -0,0 +1,122 @@ +/* + * 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 enabled by configured the + * following property: + * + *

+ * otel.instrumentation.common.enduser.enabled=true
+ * 
+ * + *

When {@code otel.instrumentation.common.enduser.enabled == true}, then each of the {@code + * enduser.*} attributes will be captured, unless they have been specifically disabled with one of + * the following properties: + * + *

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

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 enabled; + 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.enabled = + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.enabled", false); + + this.idEnabled = + this.enabled + && instrumentationConfig.getBoolean( + "otel.instrumentation.common.enduser.id.enabled", true); + this.roleEnabled = + this.enabled + && instrumentationConfig.getBoolean( + "otel.instrumentation.common.enduser.role.enabled", true); + this.scopeEnabled = + this.enabled + && instrumentationConfig.getBoolean( + "otel.instrumentation.common.enduser.scope.enabled", true); + } + + /** + * Returns true if capturing the {@code enduser.*} semantic attributes is generally enabled. + * + *

This flag is meant to control whether enduser capturing instrumentations should be applied. + * Whereas, the attribute-specific flags ({@link #isIdEnabled()}, {@link #isRoleEnabled()}, {@link + * #isScopeEnabled()}) are meant to be used by instrumentations to determine which specific + * attributes to capture. + * + *

Instrumentation implementations must also check the flags for specific attributes ({@link + * #isIdEnabled()}, {@link #isRoleEnabled()}, {@link #isScopeEnabled()}) when deciding which + * attribtues to capture. + * + * @return true if capturing the {@code enduser.*} semantic attributes is generally enabled. + */ + public boolean isEnabled() { + return this.enabled; + } + + /** + * 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 5d86cf6e9e18..a970cb5486c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -510,6 +510,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")