Skip to content

Commit

Permalink
Capture enduser attributes in Spring Security
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
philsttr committed Oct 31, 2023
1 parent b453252 commit 82c6dd9
Show file tree
Hide file tree
Showing 26 changed files with 1,412 additions and 9 deletions.
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,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. |
Original file line number Diff line number Diff line change
@@ -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")
}
}
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,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<ClassLoader> 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<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,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<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("org.springframework.security.config.web.server.ServerHttpSecurity");
}

@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

0 comments on commit 82c6dd9

Please sign in to comment.