From 82c6dd902600e057886cb6419101e0336111aea8 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 31 Oct 2023 16:28:49 +0100 Subject: [PATCH 1/7] Capture enduser attributes in Spring Security Adds library and javaagent instrumentation for spring-security-config to capture `enduser.*` semantic attributes. The library instrumentation provides: * a Servlet `Filter` and a WebFlux `WebFilter` to capture `enduser.*` semantic attributes from Spring Security `Authentication` objects. * `Customizer` implementations to insert those filters into the security filter chains created by `HttpSecurity` and `ServerHttpSecurity`, respectively. The javaagent instrumentation applies the `Customizer` implementations in the `build()` methods of `HttpSecurity` and `ServerHttpSecurity`. The automatic instrumentation is disabled by default, due to the following 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. Since this requirement is common to any automatic instrumentation that captures `enduser.*` attributes, the following new common configuration properties are introduced: * `otel.instrumentation.common.enduser.enabled` - default false. Whether to capture `enduser.*` semantic attributes. Must be set to true to capture any `enduser.*` attributes. * `otel.instrumentation.common.enduser.id.enabled` - default 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` - default 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` - default true. Whether to capture `enduser.scope` semantic attribute. Only takes effect if `otel.instrumentation.common.enduser.enabled` is true. In addition, the following new spring-security specific configuration properties are introduced: * `otel.instrumentation.spring-security.enduser.role.granted-authority-prefix` default `ROLE_`. Prefix of granted authorities identifying roles to capture in the `enduser.role` semantic attribute. * `otel.instrumentation.spring-security.enduser.scope.granted-authority-prefix` default `SCOPE_` Prefix of granted authorities identifying scopes to capture in the `enduser.scopes` semantic attribute. --- .../servlet/BaseServletHelper.java | 2 +- .../ServletAdditionalAttributesExtractor.java | 2 +- .../javaagent/README.md | 15 ++ .../javaagent/build.gradle.kts | 39 +++ .../EnduserAttributesCapturerSingletons.java | 46 ++++ .../servlet/HttpSecurityInstrumentation.java | 46 ++++ ...ityConfigServletInstrumentationModule.java | 52 ++++ .../ServerHttpSecurityInstrumentation.java | 46 ++++ ...ityConfigWebFluxInstrumentationModule.java | 48 ++++ .../HttpSecurityInstrumentationTest.java | 62 +++++ ...ServerHttpSecurityInstrumentationTest.java | 38 +++ .../library/README.md | 77 ++++++ .../library/build.gradle.kts | 20 ++ .../v6_0/EnduserAttributesCapturer.java | 122 ++++++++++ ...duserAttributesCapturingServletFilter.java | 46 ++++ ...duserAttributesHttpSecurityCustomizer.java | 37 +++ .../EnduserAttributesCapturingWebFilter.java | 51 ++++ ...ttributesServerHttpSecurityCustomizer.java | 34 +++ .../v6_0/EnduserAttributesCapturerTest.java | 226 ++++++++++++++++++ ...rAttributesCapturingServletFilterTest.java | 88 +++++++ ...rAttributesHttpSecurityCustomizerTest.java | 61 +++++ ...duserAttributesCapturingWebFilterTest.java | 86 +++++++ ...butesServerHttpSecurityCustomizerTest.java | 39 +++ .../bootstrap/internal/CommonConfig.java | 14 +- .../bootstrap/internal/EnduserConfig.java | 122 ++++++++++ settings.gradle.kts | 2 + 26 files changed, 1412 insertions(+), 9 deletions(-) create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/README.md create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/README.md create mode 100644 instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EnduserConfig.java 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..27f0e0d1633f 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; } @@ -113,8 +117,4 @@ public boolean shouldEmitExperimentalHttpClientMetrics() { 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") From 9dcfc4affac913450f2437317e4f884e9f99a735 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 31 Oct 2023 18:08:55 +0100 Subject: [PATCH 2/7] Switch enduser info capturing to specific flags for each attribute --- .../javaagent/README.md | 15 ++--- .../javaagent/build.gradle.kts | 4 +- ...ityConfigServletInstrumentationModule.java | 2 +- ...ityConfigWebFluxInstrumentationModule.java | 2 +- .../library/README.md | 12 ++-- .../v6_0/EnduserAttributesCapturer.java | 13 ++-- .../v6_0/EnduserAttributesCapturerTest.java | 65 ++++++++++--------- ...rAttributesCapturingServletFilterTest.java | 3 + ...duserAttributesCapturingWebFilterTest.java | 3 + .../bootstrap/internal/EnduserConfig.java | 53 ++++----------- 10 files changed, 81 insertions(+), 91 deletions(-) diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/README.md b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md index 83d5fa91e1df..5f303d9acca8 100644 --- a/instrumentation/spring/spring-security-config-6.0/javaagent/README.md +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md @@ -5,11 +5,10 @@ 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. | +| Property | Type | Default | Description | +|-------------------------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.common.enduser.id.enabled` | Boolean | `false` | Whether to capture `enduser.id` semantic attribute. | +| `otel.instrumentation.common.enduser.role.enabled` | Boolean | `false` | Whether to capture `enduser.role` semantic attribute. | +| `otel.instrumentation.common.enduser.scope.enabled` | Boolean | `false` | Whether to capture `enduser.scope` semantic attribute. | +| `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 index 19f0cbc99b3b..be94adc59aa2 100644 --- a/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts @@ -34,6 +34,8 @@ otelJava { tasks { test { - systemProperty("otel.instrumentation.common.enduser.enabled", "true") + 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/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 index 36b7e202b4bc..08702f628d71 100644 --- 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 @@ -32,7 +32,7 @@ public boolean defaultEnabled(ConfigProperties config) { * 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(); + return CommonConfig.get().getEnduserConfig().isAnyEnabled(); } @Override 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 index 94f2b8206a58..874d846df5d8 100644 --- 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 @@ -33,7 +33,7 @@ public boolean defaultEnabled(ConfigProperties config) { * 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(); + return CommonConfig.get().getEnduserConfig().isAnyEnabled(); } @Override diff --git a/instrumentation/spring/spring-security-config-6.0/library/README.md b/instrumentation/spring/spring-security-config-6.0/library/README.md index 700342bb605e..ab735c5586b1 100644 --- a/instrumentation/spring/spring-security-config-6.0/library/README.md +++ b/instrumentation/spring/spring-security-config-6.0/library/README.md @@ -27,9 +27,9 @@ class MyWebSecurityConfig { // 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.setEnduserIdEnabled(false); + capturer.setEnduserRoleEnabled(false); + capturer.setEnduserScopeEnabled(false); capturer.setRoleGrantedAuthorityPrefix("ROLE_"); capturer.setScopeGrantedAuthorityPrefix("SCOPE_"); @@ -62,9 +62,9 @@ class MyWebFluxSecurityConfig { // 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.setEnduserIdEnabled(false); + capturer.setEnduserRoleEnabled(false); + capturer.setEnduserScopeEnabled(false); capturer.setRoleGrantedAuthorityPrefix("ROLE_"); capturer.setScopeGrantedAuthorityPrefix("SCOPE_"); 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 index b018ffb5635b..c1d8c669603b 100644 --- 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 @@ -13,20 +13,25 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -/** Captures {@code enduser.*} semantic attributes from {@link Authentication} objects. */ +/** + * 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 set*Enabled(true)} method. + */ 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; + private boolean enduserIdEnabled; /** Determines if the {@code enduser.role} attribute should be captured. */ - private boolean enduserRoleEnabled = true; + private boolean enduserRoleEnabled; /** Determines if the {@code enduser.scope} attribute should be captured. */ - private boolean enduserScopeEnabled = true; + private boolean enduserScopeEnabled; /** The prefix used to find {@link GrantedAuthority} objects for roles. */ private String roleGrantedAuthorityPrefix = DEFAULT_ROLE_PREFIX; 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 index b0e0019d36f1..fde1b0d29d56 100644 --- 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 @@ -26,7 +26,7 @@ public class EnduserAttributesCapturerTest { @RegisterExtension InstrumentationExtension testing = LibraryInstrumentationExtension.create(); @Test - void defaults() { + void nothingEnabled() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); Authentication authentication = @@ -41,20 +41,20 @@ void defaults() { 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"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)).isNull(); }; test(capturer, authentication, assertions); } @Test - void noRoles() { + void allEnabledButNoRoles() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); Authentication authentication = new PreAuthenticatedAuthenticationToken( @@ -77,8 +77,11 @@ void noRoles() { } @Test - void noScopes() { + void allEnabledButNoScopes() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); Authentication authentication = new PreAuthenticatedAuthenticationToken( @@ -101,9 +104,9 @@ void noScopes() { } @Test - void disableEnduserId() { + void onlyEnduserIdEnabled() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); - capturer.setEnduserIdEnabled(false); + capturer.setEnduserIdEnabled(true); Authentication authentication = new PreAuthenticatedAuthenticationToken( @@ -117,20 +120,19 @@ void disableEnduserId() { 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"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)) + .isEqualTo("principal"); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)).isNull(); }; test(capturer, authentication, assertions); } @Test - void disableEnduserRole() { + void onlyEnduserRoleEnabled() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); - capturer.setEnduserRoleEnabled(false); + capturer.setEnduserRoleEnabled(true); Authentication authentication = new PreAuthenticatedAuthenticationToken( @@ -144,20 +146,19 @@ void disableEnduserRole() { 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"); + 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)).isNull(); }; test(capturer, authentication, assertions); } @Test - void disableEnduserScope() { + void onlyEnduserScopeEnabled() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); - capturer.setEnduserScopeEnabled(false); + capturer.setEnduserScopeEnabled(true); Authentication authentication = new PreAuthenticatedAuthenticationToken( @@ -171,19 +172,21 @@ void disableEnduserScope() { 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(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ID)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_ROLE)).isNull(); + assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) + .isEqualTo("scope1,scope2"); }; test(capturer, authentication, assertions); } @Test - void alternatePrefix() { + void allEnabledAndAlternatePrefix() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); capturer.setRoleGrantedAuthorityPrefix("role_"); capturer.setScopeGrantedAuthorityPrefix("scope_"); 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 index 2451948e5785..68d38dd6298f 100644 --- 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 @@ -43,6 +43,9 @@ class EnduserAttributesCapturingServletFilterTest { void test() throws Exception { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); EnduserAttributesCapturingServletFilter filter = new EnduserAttributesCapturingServletFilter(capturer); 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 index 394fb4fe0e08..44d51b330cc2 100644 --- 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 @@ -43,6 +43,9 @@ class EnduserAttributesCapturingWebFilterTest { void test() { EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); EnduserAttributesCapturingWebFilter filter = new EnduserAttributesCapturingWebFilter(capturer); testing.runWithHttpServerSpan( 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 index e1db321a560c..fbde1c90aaa2 100644 --- 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 @@ -21,21 +21,13 @@ * * * - *

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

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

- * 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
+ * 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 @@ -43,7 +35,6 @@ */ public class EnduserConfig { - private final boolean enabled; private final boolean idEnabled; private final boolean roleEnabled; private final boolean scopeEnabled; @@ -58,39 +49,23 @@ public class EnduserConfig { * * 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); + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.id.enabled", false); this.roleEnabled = - this.enabled - && instrumentationConfig.getBoolean( - "otel.instrumentation.common.enduser.role.enabled", true); + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.role.enabled", false); this.scopeEnabled = - this.enabled - && instrumentationConfig.getBoolean( - "otel.instrumentation.common.enduser.scope.enabled", true); + instrumentationConfig.getBoolean( + "otel.instrumentation.common.enduser.scope.enabled", false); } /** - * 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. + * Returns true if capturing of any {@code enduser.*} semantic attribute is enabled. * - * @return true if capturing the {@code enduser.*} semantic attributes is generally enabled. + *

This flag can be used by capturing instrumentations to bypass all {@code enduser.*} + * attribute capturing. */ - public boolean isEnabled() { - return this.enabled; + public boolean isAnyEnabled() { + return this.idEnabled || this.roleEnabled || this.scopeEnabled; } /** From e1e611c33785b753e3be0b23db2ef165b4b7ab58 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 3 Nov 2023 12:14:05 +0100 Subject: [PATCH 3/7] Add javadoc to captureEnduserAttributes --- .../v6_0/EnduserAttributesCapturer.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 index c1d8c669603b..2d5ec2c452b2 100644 --- 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 @@ -39,6 +39,29 @@ public class EnduserAttributesCapturer { /** 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 set*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); From c6c5ff86c166a674b72cd456e86ef6a93bbf3534 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 6 Nov 2023 12:12:09 +0100 Subject: [PATCH 4/7] Address review feedback --- .../javaagent/README.md | 6 ++-- ...ityConfigServletInstrumentationModule.java | 28 ++++++++++--------- ...ityConfigWebFluxInstrumentationModule.java | 24 ++++++---------- .../v6_0/EnduserAttributesCapturer.java | 6 ++-- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/README.md b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md index 5f303d9acca8..892f5367d500 100644 --- a/instrumentation/spring/spring-security-config-6.0/javaagent/README.md +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md @@ -5,10 +5,10 @@ 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.common.enduser.id.enabled` | Boolean | `false` | Whether to capture `enduser.id` semantic attribute. | -| `otel.instrumentation.common.enduser.role.enabled` | Boolean | `false` | Whether to capture `enduser.role` semantic attribute. | -| `otel.instrumentation.common.enduser.scope.enabled` | Boolean | `false` | Whether to capture `enduser.scope` semantic attribute. | | `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/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 index 08702f628d71..6e87a1ba17ca 100644 --- 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 @@ -25,24 +25,26 @@ public SpringSecurityConfigServletInstrumentationModule() { @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().isAnyEnabled(); + 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.config.annotation.web.builders.HttpSecurity") - .and( - hasClassesNamed( - "org.springframework.security.web.access.intercept.AuthorizationFilter")) - .and(hasClassesNamed("jakarta.servlet.Servlet")); + "org.springframework.security.authentication.ObservationAuthenticationManager"); } @Override 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 index 874d846df5d8..a219a7eccbd3 100644 --- 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 @@ -5,7 +5,6 @@ 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; @@ -14,7 +13,6 @@ 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) @@ -26,19 +24,15 @@ public SpringSecurityConfigWebFluxInstrumentationModule() { @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().isAnyEnabled(); - } - - @Override - public ElementMatcher.Junction classLoaderMatcher() { - return hasClassesNamed("org.springframework.security.config.web.server.ServerHttpSecurity"); + 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 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 index 2d5ec2c452b2..7e7ab3ef0af9 100644 --- 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 @@ -17,9 +17,9 @@ * 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 set*Enabled(true)} method. + * the appropriate {@code setEnduser*Enabled(true)} method. */ -public class EnduserAttributesCapturer { +public final class EnduserAttributesCapturer { private static final String DEFAULT_ROLE_PREFIX = "ROLE_"; private static final String DEFAULT_SCOPE_PREFIX = "SCOPE_"; @@ -43,7 +43,7 @@ public class EnduserAttributesCapturer { * 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 set*Enabled(true)} methods are captured. + *

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

The following attributes can be captured: * From fb9ba0a58f970df30fb22fc12f9cf7dae130b0c2 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 13 Nov 2023 14:34:22 +0100 Subject: [PATCH 5/7] Remove unneed dependency from spring-security-config instrumentation --- .../spring-security-config-6.0/javaagent/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) 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 index be94adc59aa2..3bbbfeeca982 100644 --- a/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts @@ -15,8 +15,6 @@ muzzle { } dependencies { - bootstrap(project(":instrumentation:executors:bootstrap")) - implementation(project(":instrumentation:spring:spring-security-config-6.0:library")) implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") From 3e064cf0625611f6245fbe5f2c8c7be4a0946f5d Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 13 Nov 2023 15:37:33 +0100 Subject: [PATCH 6/7] Remove extension api dependency and switch some test dependencies to testLibrary --- .../spring-security-config-6.0/javaagent/build.gradle.kts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 3bbbfeeca982..f30b3eaad1f3 100644 --- a/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts @@ -16,14 +16,13 @@ muzzle { dependencies { 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") + testLibrary("org.springframework:spring-test:6.0.0") + testLibrary("jakarta.servlet:jakarta.servlet-api:6.0.0") } otelJava { From d4d1cfa54dadcf3c9181179d6c2681cdb5a77f12 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 13 Nov 2023 15:38:14 +0100 Subject: [PATCH 7/7] wait for spans matching criteria to give spans time to be exported --- .../v6_0/EnduserAttributesCapturerTest.java | 134 ++++++++---------- ...rAttributesCapturingServletFilterTest.java | 23 +-- ...duserAttributesCapturingWebFilterTest.java | 23 +-- 3 files changed, 75 insertions(+), 105 deletions(-) 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 index fde1b0d29d56..42720f9a3834 100644 --- 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 @@ -5,16 +5,16 @@ package io.opentelemetry.instrumentation.spring.security.config.v6_0; -import static org.assertj.core.api.Assertions.assertThat; - +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.List; -import org.assertj.core.api.ThrowingConsumer; +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; @@ -39,14 +39,13 @@ void nothingEnabled() { 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)).isNull(); - assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)).isNull(); - }; - - test(capturer, authentication, assertions); + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(SemanticAttributes.ENDUSER_ID)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); } @Test @@ -64,16 +63,13 @@ void allEnabledButNoRoles() { 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( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2")); } @Test @@ -91,16 +87,13 @@ void allEnabledButNoScopes() { 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( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); } @Test @@ -118,15 +111,13 @@ void onlyEnduserIdEnabled() { 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)).isNull(); - }; - - test(capturer, authentication, assertions); + test( + capturer, + authentication, + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); } @Test @@ -144,15 +135,13 @@ void onlyEnduserRoleEnabled() { 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)).isNull(); - }; - - test(capturer, authentication, assertions); + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(SemanticAttributes.ENDUSER_ID)) + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .doesNotHave(attribute(SemanticAttributes.ENDUSER_SCOPE))); } @Test @@ -170,15 +159,13 @@ void onlyEnduserScopeEnabled() { 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)).isNull(); - assertThat(span.getAttributes().get(SemanticAttributes.ENDUSER_SCOPE)) - .isEqualTo("scope1,scope2"); - }; - - test(capturer, authentication, assertions); + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(SemanticAttributes.ENDUSER_ID)) + .doesNotHave(attribute(SemanticAttributes.ENDUSER_ROLE)) + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2")); } @Test @@ -200,30 +187,31 @@ void allEnabledAndAlternatePrefix() { 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( + 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, - ThrowingConsumer assertions) { + Consumer assertions) { testing.runWithHttpServerSpan( () -> { Context otelContext = Context.current(); capturer.captureEnduserAttributes(otelContext, authentication); }); - List spans = testing.spans(); - assertThat(spans).singleElement().satisfies(assertions); + 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 index 68d38dd6298f..792764047412 100644 --- 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 @@ -5,18 +5,14 @@ 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; @@ -75,17 +71,12 @@ void test() throws Exception { } }); - 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"); - }); + 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/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 index 44d51b330cc2..1db46c41c6e2 100644 --- 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 @@ -5,18 +5,14 @@ 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; @@ -73,17 +69,12 @@ void test() { .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"); - }); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute(SemanticAttributes.ENDUSER_ID, "principal") + .hasAttribute(SemanticAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(SemanticAttributes.ENDUSER_SCOPE, "scope1,scope2"))); } }