Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture enduser attributes in Spring Security #9777

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void onEnd(
ServletRequestContext<REQUEST> requestContext,
@Nullable ServletResponseContext<RESPONSE> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# OpenTelemetry Javaagent Instrumentation: Spring Security Config

Javaagent automatic instrumentation to capture `enduser.*` semantic attributes
from Spring Security `Authentication` objects.

## Settings

This module honors the [common `otel.instrumentation.common.enduser.*` properties](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#common-instrumentation-configuration)
and the following properties:

| Property | Type | Default | Description |
|-------------------------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------|
| `otel.instrumentation.spring-security.enduser.role.granted-authority-prefix` | String | `ROLE_` | Prefix of granted authorities identifying roles to capture in the `enduser.role` semantic attribute. |
| `otel.instrumentation.spring-security.enduser.scope.granted-authority-prefix` | String | `SCOPE_` | Prefix of granted authorities identifying scopes to capture in the `enduser.scopes` semantic attribute. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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"))
philsttr marked this conversation as resolved.
Show resolved Hide resolved

implementation(project(":instrumentation:spring:spring-security-config-6.0:library"))
implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
philsttr marked this conversation as resolved.
Show resolved Hide resolved

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")
philsttr marked this conversation as resolved.
Show resolved Hide resolved
testImplementation("jakarta.servlet:jakarta.servlet-api:6.0.0")
}

otelJava {
minJavaVersionSupported.set(JavaVersion.VERSION_17)
}

tasks {
test {
systemProperty("otel.instrumentation.common.enduser.id.enabled", "true")
systemProperty("otel.instrumentation.common.enduser.role.enabled", "true")
systemProperty("otel.instrumentation.common.enduser.scope.enabled", "true")
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

/** Instrumentation module for servlet-based applications that use spring-security-config. */
@AutoService(InstrumentationModule.class)
public class SpringSecurityConfigServletInstrumentationModule extends InstrumentationModule {
public SpringSecurityConfigServletInstrumentationModule() {
super("spring-security-config-servlet", "spring-security-config-servlet-6.0");
}

@Override
public boolean defaultEnabled(ConfigProperties config) {
return super.defaultEnabled(config)
/*
* Since the only thing this module currently does is capture enduser attributes,
* the module can be completely disabled if enduser attributes are disabled.
*
* If any functionality not related to enduser attributes is added to this module,
* then this check will need to move elsewhere to only guard the enduser attributes logic.
*/
&& CommonConfig.get().getEnduserConfig().isAnyEnabled();
}

@Override
public ElementMatcher.Junction<ClassLoader> 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(
philsttr marked this conversation as resolved.
Show resolved Hide resolved
"org.springframework.security.authentication.ObservationAuthenticationManager");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpSecurityInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux;

import static java.util.Collections.singletonList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.util.List;

/** Instrumentation module for webflux-based applications that use spring-security-config. */
@AutoService(InstrumentationModule.class)
public class SpringSecurityConfigWebFluxInstrumentationModule extends InstrumentationModule {

public SpringSecurityConfigWebFluxInstrumentationModule() {
super("spring-security-config-webflux", "spring-security-config-webflux-6.0");
}

@Override
public boolean defaultEnabled(ConfigProperties config) {
return super.defaultEnabled(config)
/*
* Since the only thing this module currently does is capture enduser attributes,
* the module can be completely disabled if enduser attributes are disabled.
*
* If any functionality not related to enduser attributes is added to this module,
* then this check will need to move elsewhere to only guard the enduser attributes logic.
*/
&& CommonConfig.get().getEnduserConfig().isAnyEnabled();
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new ServerHttpSecurityInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> objectPostProcessor;

/**
* Ensures that {@link HttpSecurityInstrumentation} registers a {@link
* EnduserAttributesCapturingServletFilter} in the filter chain.
*
* <p>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);
}
}
Loading
Loading