diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc
index 5462114043b5b..cb4b024817441 100644
--- a/docs/src/main/asciidoc/opentelemetry.adoc
+++ b/docs/src/main/asciidoc/opentelemetry.adoc
@@ -398,6 +398,25 @@ public class CustomConfiguration {
}
----
+==== End User attributes
+
+When enabled, Quarkus adds OpenTelemetry End User attributes as Span attributes.
+The attributes are only added when authentication has already happened on a best-efforts basis.
+Before you enable this feature, verify that Quarkus Security extension is present and configured.
+More information about the Quarkus Security can be found in the xref:security-overview.adoc[Quarkus Security overview].
+
+[source,application.properties]
+----
+quarkus.otel.traces.eusp.enabled=true <1>
+quarkus.http.auth.proactive=true <2>
+----
+<1> Enable the End User Attributes feature so that the `SecurityIdentity` principal and roles are added as Span attributes.
+The End User attributes are personally identifiable information, therefore make sure you want to export them before you enable this feature.
+<2> Optionally enable proactive authentication.
+The best possible results are achieved when proactive authentication is enabled because the authentication happens sooner.
+
+IMPORTANT: This feature is not supported when a custom xref:security-customization.adoc#jaxrs-security-context[Jakarta REST SecurityContexts] is used.
+
[[sampler]]
=== Sampler
A https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling[sampler] decides whether a trace should be discarded or forwarded, effectively managing noise and reducing overhead by limiting the number of collected traces sent to the collector.
diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
index 14f09b0a37753..0afb569448249 100644
--- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
+++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
@@ -1,6 +1,9 @@
package io.quarkus.opentelemetry.deployment.tracing;
import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.ALL;
+import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHENTICATION_SUCCESS;
+import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHORIZATION_FAILURE;
+import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHORIZATION_SUCCESS;
import java.net.URL;
import java.util.ArrayList;
@@ -11,6 +14,7 @@
import java.util.Set;
import java.util.function.BooleanSupplier;
+import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.spi.EventContext;
import org.jboss.jandex.AnnotationInstance;
@@ -21,12 +25,16 @@
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;
+import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.IdGenerator;
+import io.opentelemetry.sdk.trace.ReadWriteSpan;
+import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
+import io.quarkus.arc.Unremovable;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem;
@@ -49,6 +57,7 @@
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType;
import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder;
import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer;
+import io.quarkus.opentelemetry.runtime.tracing.security.EndUserSpanProcessor;
import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil;
import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem;
import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem;
@@ -198,6 +207,70 @@ void registerSecurityEventObserver(Capabilities capabilities, OTelBuildConfig bu
}
}
+ /**
+ * Generates {@link SpanProcessor} that adds end-user attributes to new Spans.
+ *
+ * Generates:
+ *
+ *
+ * import io.quarkus.arc.Unremovable;
+ * import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil;
+ * import jakarta.enterprise.context.Dependent;
+ *
+ * import io.opentelemetry.context.Context;
+ * import io.opentelemetry.sdk.trace.ReadWriteSpan;
+ * import io.opentelemetry.sdk.trace.ReadableSpan;
+ * import io.opentelemetry.sdk.trace.SpanProcessor;
+ *
+ * @Dependent
+ * @Unremovable
+ * public class EndUserSpanProcessor implements SpanProcessor {
+ *
+ * @Override
+ * public void onStart(Context parentContext, ReadWriteSpan span) {
+ * SecurityEventUtil.addEndUserAttributes(span);
+ * }
+ *
+ * @Override
+ * public boolean isStartRequired() {
+ * return Boolean.TRUE;
+ * }
+ *
+ * @Override
+ * public void onEnd(ReadableSpan span) {
+ * }
+ *
+ * @Override
+ * public boolean isEndRequired() {
+ * return Boolean.FALSE;
+ * }
+ *
+ * }
+ *
+ *
+ */
+ @BuildStep(onlyIf = EndUserAttributesEnabled.class)
+ void addEndUserAttributesSpanProcessor(BuildProducer additionalBeanProducer,
+ Capabilities capabilities) {
+ if (capabilities.isPresent(Capability.SECURITY)) {
+ additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(EndUserSpanProcessor.class));
+ }
+ }
+
+ @BuildStep(onlyIf = EndUserAttributesEnabled.class)
+ void registerEndUserAttributesEventObserver(Capabilities capabilities,
+ ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
+ BuildProducer observerProducer) {
+ if (capabilities.isPresent(Capability.SECURITY)) {
+ observerProducer
+ .produce(createEventObserver(observerRegistrationPhase, AUTHENTICATION_SUCCESS, "addEndUserAttributes"));
+ observerProducer
+ .produce(createEventObserver(observerRegistrationPhase, AUTHORIZATION_SUCCESS, "updateEndUserAttributes"));
+ observerProducer
+ .produce(createEventObserver(observerRegistrationPhase, AUTHORIZATION_FAILURE, "updateEndUserAttributes"));
+ }
+ }
+
private static ObserverConfiguratorBuildItem createEventObserver(
ObserverRegistrationPhaseBuildItem observerRegistrationPhase, SecurityEventType eventType, String utilMethodName) {
return new ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext()
@@ -232,4 +305,18 @@ public boolean getAsBoolean() {
return enabled;
}
}
+
+ static final class EndUserAttributesEnabled implements BooleanSupplier {
+
+ private final boolean enabled;
+
+ EndUserAttributesEnabled(OTelBuildConfig config) {
+ this.enabled = config.traces().addEndUserAttributes();
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return enabled;
+ }
+ }
}
diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java
index 1c53611a14f1a..d387253747d62 100644
--- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java
+++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java
@@ -7,6 +7,7 @@
import io.quarkus.runtime.annotations.ConfigGroup;
import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithName;
/**
* Tracing build time configuration
@@ -51,4 +52,15 @@ public interface TracesBuildConfig {
*/
@WithDefault(SamplerType.Constants.PARENT_BASED_ALWAYS_ON)
String sampler();
+
+ /**
+ * If OpenTelemetry End User attributes should be added as Span attributes on a best-efforts basis.
+ *
+ * @see OpenTelemetry End User
+ * attributes
+ */
+ @WithName("eusp.enabled")
+ @WithDefault("false")
+ boolean addEndUserAttributes();
+
}
diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java
new file mode 100644
index 0000000000000..4660eced55417
--- /dev/null
+++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java
@@ -0,0 +1,35 @@
+package io.quarkus.opentelemetry.runtime.tracing.security;
+
+import jakarta.enterprise.context.Dependent;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.ReadWriteSpan;
+import io.opentelemetry.sdk.trace.ReadableSpan;
+import io.opentelemetry.sdk.trace.SpanProcessor;
+
+/**
+ * Main purpose of this processor is to cover adding of the End User attributes to user-created Spans.
+ */
+@Dependent
+public class EndUserSpanProcessor implements SpanProcessor {
+
+ @Override
+ public void onStart(Context context, ReadWriteSpan span) {
+ SecurityEventUtil.addEndUserAttributes(span);
+ }
+
+ @Override
+ public boolean isStartRequired() {
+ return true;
+ }
+
+ @Override
+ public void onEnd(ReadableSpan readableSpan) {
+
+ }
+
+ @Override
+ public boolean isEndRequired() {
+ return false;
+ }
+}
diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java
index 674ded182b212..e7cb22987e320 100644
--- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java
+++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java
@@ -9,6 +9,8 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.semconv.SemanticAttributes;
import io.quarkus.arc.Arc;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
@@ -16,10 +18,13 @@
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEvent;
+import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
+import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
+import io.vertx.ext.web.RoutingContext;
/**
* Synthetic CDI observers for various {@link SecurityEvent} types configured during the build time use this util class
- * to export the events as the OpenTelemetry Span events.
+ * to export the events as the OpenTelemetry Span events, or authenticated user Span attributes.
*/
public final class SecurityEventUtil {
public static final String QUARKUS_SECURITY_NAMESPACE = "quarkus.security.";
@@ -38,8 +43,58 @@ private SecurityEventUtil() {
// UTIL CLASS
}
+ /**
+ * Adds Span attributes describing authenticated user if the user is authenticated and CDI request context is active.
+ * This will be true for example inside JAX-RS resources when the CDI request context is already setup and user code
+ * creates a new Span.
+ *
+ * @param span valid and recording Span; must not be null
+ */
+ static void addEndUserAttributes(Span span) {
+ if (Arc.container().requestContext().isActive()) {
+ var currentVertxRequest = Arc.container().instance(CurrentVertxRequest.class).get();
+ if (currentVertxRequest.getCurrent() != null) {
+ addEndUserAttribute(currentVertxRequest.getCurrent(), span);
+ }
+ }
+ }
+
+ /**
+ * Updates authenticated user Span attributes if the {@link SecurityIdentity} got augmented during authorization.
+ *
+ * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
+ *
+ * @param event {@link AuthorizationFailureEvent}
+ */
+ public static void updateEndUserAttributes(AuthorizationFailureEvent event) {
+ addEndUserAttribute(event.getSecurityIdentity(), getSpan());
+ }
+
+ /**
+ * Updates authenticated user Span attributes if the {@link SecurityIdentity} got augmented during authorization.
+ *
+ * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
+ *
+ * @param event {@link AuthorizationSuccessEvent}
+ */
+ public static void updateEndUserAttributes(AuthorizationSuccessEvent event) {
+ addEndUserAttribute(event.getSecurityIdentity(), getSpan());
+ }
+
+ /**
+ * If there is already valid recording {@link Span}, attributes describing authenticated user are added to it.
+ *
+ * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
+ *
+ * @param event {@link AuthenticationSuccessEvent}
+ */
+ public static void addEndUserAttributes(AuthenticationSuccessEvent event) {
+ addEndUserAttribute(event.getSecurityIdentity(), getSpan());
+ }
+
/**
* Adds {@link SecurityEvent} as Span event.
+ *
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addAllEvents(SecurityEvent event) {
@@ -57,6 +112,8 @@ public static void addAllEvents(SecurityEvent event) {
}
/**
+ * Adds {@link AuthenticationSuccessEvent} as Span event.
+ *
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthenticationSuccessEvent event) {
@@ -64,6 +121,8 @@ public static void addEvent(AuthenticationSuccessEvent event) {
}
/**
+ * Adds {@link AuthenticationFailureEvent} as Span event.
+ *
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthenticationFailureEvent event) {
@@ -71,6 +130,8 @@ public static void addEvent(AuthenticationFailureEvent event) {
}
/**
+ * Adds {@link AuthorizationSuccessEvent} as Span event.
+ *
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthorizationSuccessEvent event) {
@@ -79,6 +140,8 @@ public static void addEvent(AuthorizationSuccessEvent event) {
}
/**
+ * Adds {@link AuthorizationFailureEvent} as Span event.
+ *
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthorizationFailureEvent event) {
@@ -88,6 +151,7 @@ public static void addEvent(AuthorizationFailureEvent event) {
/**
* Adds {@link SecurityEvent} as Span event that is not authN/authZ success/failure.
+ *
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(SecurityEvent event) {
@@ -112,15 +176,14 @@ public void accept(String key, Object value) {
}
private static void addEvent(String eventName, Attributes attributes) {
- Span span = Arc.container().select(Span.class).get();
- if (span.getSpanContext().isValid() && span.isRecording()) {
+ Span span = getSpan();
+ if (spanIsValidAndRecording(span)) {
span.addEvent(eventName, attributes, Instant.now());
}
}
private static AttributesBuilder attributesBuilder(SecurityEvent event, String failureKey) {
- Throwable failure = (Throwable) event.getEventProperties().get(failureKey);
- if (failure != null) {
+ if (event.getEventProperties().get(failureKey) instanceof Throwable failure) {
return attributesBuilder(event).put(FAILURE_NAME, failure.getClass().getName());
}
return attributesBuilder(event);
@@ -146,4 +209,55 @@ private static Attributes withAuthorizationContext(SecurityEvent event, Attribut
}
return builder.build();
}
+
+ /**
+ * Adds Span attributes describing the authenticated user.
+ *
+ * @param event {@link RoutingContext}; must not be null
+ * @param span valid recording Span; must not be null
+ */
+ private static void addEndUserAttribute(RoutingContext event, Span span) {
+ if (event.user() instanceof QuarkusHttpUser user) {
+ addEndUserAttribute(user.getSecurityIdentity(), span);
+ }
+ }
+
+ /**
+ * Adds End User attributes to the {@code span}. Only authenticated user is added to the {@link Span}.
+ * Anonymous identity is ignored as it does not represent authenticated user.
+ * Passed {@code securityIdentity} is attached to the {@link Context} so that we recognize when identity changes.
+ *
+ * @param securityIdentity SecurityIdentity
+ * @param span Span
+ */
+ private static void addEndUserAttribute(SecurityIdentity securityIdentity, Span span) {
+ if (securityIdentity != null && !securityIdentity.isAnonymous() && spanIsValidAndRecording(span)) {
+ span.setAllAttributes(Attributes.of(
+ SemanticAttributes.ENDUSER_ID,
+ securityIdentity.getPrincipal().getName(),
+ SemanticAttributes.ENDUSER_ROLE,
+ getRoles(securityIdentity)));
+ }
+ }
+
+ private static String getRoles(SecurityIdentity securityIdentity) {
+ try {
+ return securityIdentity.getRoles().toString();
+ } catch (UnsupportedOperationException e) {
+ // getting roles is not supported when the identity is enhanced by custom jakarta.ws.rs.core.SecurityContext
+ return "";
+ }
+ }
+
+ private static Span getSpan() {
+ if (Arc.container().requestContext().isActive()) {
+ return Arc.container().select(Span.class).get();
+ } else {
+ return Span.current();
+ }
+ }
+
+ private static boolean spanIsValidAndRecording(Span span) {
+ return span.isRecording() && span.getSpanContext().isValid();
+ }
}
diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java
new file mode 100644
index 0000000000000..e3dead7ad460b
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java
@@ -0,0 +1,43 @@
+package io.quarkus.it.opentelemetry.reactive;
+
+import java.util.Map;
+
+import jakarta.inject.Singleton;
+
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.SecurityIdentityAugmentor;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
+import io.smallrye.mutiny.Uni;
+
+@Singleton
+public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
+ @Override
+ public Uni augment(SecurityIdentity securityIdentity,
+ AuthenticationRequestContext authenticationRequestContext) {
+ return augment(securityIdentity, authenticationRequestContext, Map.of());
+ }
+
+ @Override
+ public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context,
+ Map attributes) {
+ var routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
+ if (routingContext != null) {
+ var augmentorScenario = routingContext.normalizedPath().contains("-augmentor");
+ var configRolesMappingScenario = routingContext.normalizedPath().contains("roles-mapping-http-perm");
+ if (augmentorScenario || configRolesMappingScenario) {
+ var builder = QuarkusSecurityIdentity.builder(identity);
+ if (augmentorScenario) {
+ builder.addRole("AUGMENTOR");
+ }
+ if (configRolesMappingScenario) {
+ // this role is supposed to be re-mapped by HTTP roles mapping (not path-specific)
+ builder.addRole("ROLES-ALLOWED-MAPPING-ROLE");
+ }
+ return Uni.createFrom().item(builder.build());
+ }
+ }
+ return Uni.createFrom().item(identity);
+ }
+}
diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java
new file mode 100644
index 0000000000000..ea8765c35c87e
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java
@@ -0,0 +1,112 @@
+package io.quarkus.it.opentelemetry.reactive;
+
+import jakarta.annotation.security.PermitAll;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.semconv.SemanticAttributes;
+
+@Path("/otel/enduser")
+public class EndUserResource {
+
+ @Inject
+ Tracer tracer;
+
+ @Path("/no-authorization")
+ @GET
+ public String noAuthorization() {
+ return "/no-authorization";
+ }
+
+ @RolesAllowed("WRITER")
+ @Path("/roles-allowed-only-writer-role")
+ @GET
+ public String rolesAllowedOnlyWriterRole() {
+ return "/roles-allowed-only-writer-role";
+ }
+
+ @PermitAll
+ @Path("/permit-all-only")
+ @GET
+ public String permitAllOnly() {
+ return "/permit-all-only";
+ }
+
+ @Path("/no-authorization-augmentor")
+ @GET
+ public String noAuthorizationAugmentor() {
+ return "/no-authorization-augmentor";
+ }
+
+ @RolesAllowed("AUGMENTOR")
+ @Path("/roles-allowed-only-augmentor-role")
+ @GET
+ public String rolesAllowedOnlyAugmentorRole() {
+ return "/roles-allowed-only-augmentor-role";
+ }
+
+ @PermitAll
+ @Path("/permit-all-only-augmentor")
+ @GET
+ public String permitAllOnlyAugmentor() {
+ return "/permit-all-only-augmentor";
+ }
+
+ @RolesAllowed("WRITER-HTTP-PERM")
+ @Path("/roles-allowed-writer-http-perm-role")
+ @GET
+ public String rolesAllowedHttpPermWriterHttpPermRole() {
+ return "/roles-allowed-writer-http-perm-role";
+ }
+
+ @PermitAll
+ @Path("/roles-mapping-http-perm")
+ @GET
+ public String permitAllAnnotationConfigRolesMappingPermitAllHttpPerm() {
+ return "/roles-mapping-http-perm";
+ }
+
+ @RolesAllowed("HTTP-PERM-AUGMENTOR")
+ @Path("/roles-allowed-http-perm-augmentor-role")
+ @GET
+ public String rolesAllowedHttpPermHttpAugmentorPermRole() {
+ return "/roles-allowed-http-perm-augmentor-role";
+ }
+
+ @PermitAll
+ @Path("/roles-mapping-http-perm-augmentor")
+ @GET
+ public String permitAllAnnotationConfigRolesMappingPermitAllHttpPermAugmentor() {
+ return "/roles-mapping-http-perm-augmentor";
+ }
+
+ @Path("/jax-rs-http-perm")
+ @GET
+ public String jaxRsHttpPermOnly() {
+ return "/jax-rs-http-perm";
+ }
+
+ @RolesAllowed("READER")
+ @Path("/jax-rs-http-perm-annotation-reader-role")
+ @GET
+ public String jaxRsHttpPermRolesAllowedReaderRole() {
+ return "/jax-rs-http-perm-annotation-reader-role";
+ }
+
+ @RolesAllowed("READER")
+ @Path("/custom-span-reader-role")
+ @GET
+ public String customSpanReaderRole() {
+ var span = tracer.spanBuilder("custom-span").startSpan();
+ try (var ignored = span.makeCurrent()) {
+ span.setAttribute("custom_attribute", "custom-value");
+ span.setAttribute(SemanticAttributes.HTTP_TARGET, "custom-path");
+ } finally {
+ span.end();
+ }
+ return "/custom-span-reader-role";
+ }
+}
diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java
new file mode 100644
index 0000000000000..5cda036ab3688
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java
@@ -0,0 +1,233 @@
+package io.quarkus.it.opentelemetry.reactive.enduser;
+
+import static io.restassured.RestAssured.get;
+import static io.restassured.RestAssured.given;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Set;
+
+import org.awaitility.Awaitility;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.opentelemetry.semconv.SemanticAttributes;
+import io.quarkus.it.opentelemetry.reactive.Utils;
+
+public abstract class AbstractEndUserTest {
+
+ private static final String END_USER_ID_ATTR = SemanticAttributes.ENDUSER_ID.getKey();
+ private static final String END_USER_ROLE_ATTR = SemanticAttributes.ENDUSER_ROLE.getKey();
+ private static final String HTTP_PERM_AUGMENTOR = "HTTP-PERM-AUGMENTOR";
+ protected static final String READER_ROLE = "READER";
+ protected static final String WRITER_ROLE = "WRITER";
+ protected static final String AUGMENTOR_ROLE = "AUGMENTOR";
+
+ /**
+ * This is 'ROLES-ALLOWED-MAPPING-ROLE' role granted to the SecureIdentity by augmentor and
+ * remapped to 'ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM' role which allows to verify that the
+ * 'quarkus.http.auth.roles-mapping' config-level roles mapping is reflected in the End User attributes.
+ */
+ protected static final String HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE = "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM";
+
+ @BeforeEach
+ @AfterEach
+ public void reset() {
+ given().get("/reset").then().statusCode(HTTP_OK);
+ await().atMost(5, SECONDS).until(() -> Utils.getSpans().isEmpty());
+ }
+
+ @Test
+ public void testAttributesWhenNoAuthorizationInPlace() {
+ assertEndUserAttributes("/no-authorization", isProactiveAuthEnabled(), User.SCOTT, null);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationOnlyUnauthorized() {
+ assertEndUserAttributes("/roles-allowed-only-writer-role", true, User.SCOTT, WRITER_ROLE);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationOnlyAuthorized() {
+ assertEndUserAttributes("/roles-allowed-only-writer-role", true, User.STUART, WRITER_ROLE);
+ }
+
+ @Test
+ public void testWhenPermitAllOnly() {
+ assertEndUserAttributes("/permit-all-only", isProactiveAuthEnabled(), User.STUART, null);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationOnlyAuthorizedAugmentor() {
+ assertEndUserAttributes("/roles-allowed-only-augmentor-role", true, User.SCOTT, AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testWhenPermitAllOnlyAugmentor() {
+ assertEndUserAttributes("/permit-all-only-augmentor", isProactiveAuthEnabled(), User.STUART, AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testAttributesWhenNoAuthorizationInPlaceAugmentor() {
+ assertEndUserAttributes("/no-authorization-augmentor", isProactiveAuthEnabled(), User.SCOTT,
+ AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testWhenConfigRolesMappingHttpPermAugmentor() {
+ assertEndUserAttributes("/roles-mapping-http-perm-augmentor", isProactiveAuthEnabled(), User.STUART,
+ null, HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE, AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testWhenConfigRolesMappingHttpPerm() {
+ assertEndUserAttributes("/roles-mapping-http-perm", isProactiveAuthEnabled(), User.STUART, null,
+ HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationHttpPermUnauthorized() {
+ assertEndUserAttributes("/roles-allowed-writer-http-perm-role", true, User.SCOTT, WRITER_ROLE, "AUTHZ-FAILURE-ROLE");
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationHttpPermAuthorized() {
+ assertEndUserAttributes("/roles-allowed-writer-http-perm-role", true, User.STUART, WRITER_ROLE,
+ WRITER_ROLE + "-HTTP-PERM");
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationHttpPermAuthorizedAugmentor() {
+ assertEndUserAttributes("/roles-allowed-http-perm-augmentor-role", true, User.STUART, AUGMENTOR_ROLE,
+ HTTP_PERM_AUGMENTOR);
+ }
+
+ @Test
+ public void testJaxRsHttpPermOnlyAuthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm", true, User.STUART, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testJaxRsHttpPermOnlyUnauthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm", true, User.SCOTT, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testJaxRsHttpPermAndRolesAllowedAnnotationAuthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm-annotation-reader-role", true, User.STUART, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testJaxRsHttpPermAndRolesAllowedAnnotationUnauthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm-annotation-reader-role", true, User.SCOTT, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testCustomSpanContainsEndUserAttributes() {
+ var path = "/custom-span-reader-role";
+ assertEndUserAttributes(path, true, User.SCOTT, READER_ROLE);
+
+ // assert custom span also contains end user attributes
+ var spanData = waitForSpanWithPath("custom-path");
+ assertEquals("custom-value", spanData.get("custom_attribute"), spanData.toString());
+ assertEquals(User.SCOTT.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString());
+ String role = spanData.get(END_USER_ROLE_ATTR);
+ assertNotNull(role, spanData.toString());
+ assertTrue(role.contains("READER"), spanData.toString());
+ }
+
+ protected void assertEndUserAttributes(String subPath, boolean expectEndUserAttrs, User requestUser, String requiredRole,
+ String... extraRoles) {
+ var path = "/otel/enduser" + subPath;
+ var response = given()
+ .when()
+ .auth().preemptive().basic(requestUser.userName(), requestUser.password)
+ .get(path)
+ .then();
+
+ boolean augmentorScenario = AUGMENTOR_ROLE.equals(requiredRole);
+ boolean accessGranted = augmentorScenario || requiredRole == null || requestUser.roles.contains(requiredRole);
+ if (accessGranted) {
+ response.statusCode(200).body(is(subPath));
+ } else {
+ response.statusCode(403);
+ }
+
+ var spanData = waitForSpanWithPath(path);
+ if (expectEndUserAttrs) {
+ assertEquals(requestUser.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString());
+ String role = spanData.get(END_USER_ROLE_ATTR);
+ assertNotNull(role, spanData.toString());
+
+ if (augmentorScenario) {
+ assertTrue(role.contains(AUGMENTOR_ROLE));
+ assertTrue(role.contains(AUGMENTOR_ROLE));
+ }
+
+ assertTrue(requestUser.roles.stream().allMatch(role::contains), spanData.toString());
+
+ for (String extraRole : extraRoles) {
+ assertTrue(role.contains(extraRole), spanData.toString());
+ }
+ } else {
+ assertNull(spanData.get(END_USER_ID_ATTR), spanData.toString());
+ assertNull(spanData.get(END_USER_ROLE_ATTR), spanData.toString());
+ }
+ }
+
+ protected Map waitForSpanWithPath(final String path) {
+ Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> !getSpanByPath(path).isEmpty(), new TypeSafeMatcher<>() {
+ @Override
+ protected boolean matchesSafely(Boolean aBoolean) {
+ return Boolean.TRUE.equals(aBoolean);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Span with the 'http.target' attribute not found: " + path + " ; " + Utils.getSpans());
+ }
+ });
+ return getSpanByPath(path);
+ }
+
+ private Map getSpanByPath(final String path) {
+ return Utils
+ .getSpans()
+ .stream()
+ .map(m -> (Map) m.get("attributes"))
+ .filter(m -> path.equals(m.get(SemanticAttributes.HTTP_TARGET.getKey())))
+ .findFirst()
+ .orElse(Map.of());
+ }
+
+ protected abstract boolean isProactiveAuthEnabled();
+
+ public enum User {
+
+ SCOTT("reader", Set.of(READER_ROLE)),
+ STUART("writer", Set.of(READER_ROLE, WRITER_ROLE));
+
+ private final String password;
+ private final Set roles;
+
+ User(String password, Set roles) {
+ this.password = password;
+ this.roles = roles;
+ }
+
+ private String userName() {
+ return this.toString().toLowerCase();
+ }
+ }
+}
diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java
new file mode 100644
index 0000000000000..1d894e70f9209
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java
@@ -0,0 +1,15 @@
+package io.quarkus.it.opentelemetry.reactive.enduser;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+
+@QuarkusTest
+@TestProfile(EndUserProfile.class)
+public class EagerAuthEndUserEnabledTest extends AbstractEndUserTest {
+
+ @Override
+ protected boolean isProactiveAuthEnabled() {
+ return true;
+ }
+
+}
diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java
new file mode 100644
index 0000000000000..3cd13f99dc8ac
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java
@@ -0,0 +1,33 @@
+package io.quarkus.it.opentelemetry.reactive.enduser;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.quarkus.test.junit.QuarkusTestProfile;
+
+public class EndUserProfile implements QuarkusTestProfile {
+
+ @Override
+ public Map getConfigOverrides() {
+ Map config = new HashMap<>(Map.of("quarkus.otel.traces.eusp.enabled", "true",
+ "quarkus.http.auth.permission.roles-1.policy", "role-policy-1",
+ "quarkus.http.auth.permission.roles-1.paths", "/otel/enduser/roles-allowed-writer-http-perm-role",
+ "quarkus.http.auth.policy.role-policy-1.roles.WRITER", "WRITER-HTTP-PERM"));
+ config.put("quarkus.http.auth.policy.role-policy-1.roles.READER", "AUTHZ-FAILURE-ROLE");
+ config.put("quarkus.http.auth.policy.role-policy-1.roles-allowed", "WRITER");
+ config.put("quarkus.http.auth.permission.roles-2.policy", "role-policy-2");
+ config.put("quarkus.http.auth.permission.roles-2.paths", "/otel/enduser/roles-allowed-http-perm-augmentor-role");
+ config.put("quarkus.http.auth.policy.role-policy-2.roles-allowed", "AUGMENTOR");
+ config.put("quarkus.http.auth.policy.role-policy-2.roles.AUGMENTOR", "HTTP-PERM-AUGMENTOR");
+ config.put("quarkus.http.auth.permission.jax-rs.policy", "jax-rs");
+ config.put("quarkus.http.auth.permission.jax-rs.paths",
+ "/otel/enduser/jax-rs-http-perm,/otel/enduser/jax-rs-http-perm-annotation-reader-role");
+ config.put("quarkus.http.auth.permission.jax-rs.applies-to", "JAXRS");
+ config.put("quarkus.http.auth.policy.jax-rs.roles-allowed", "WRITER");
+ config.put("quarkus.http.auth.permission.permit-all.policy", "permit");
+ config.put("quarkus.http.auth.permission.permit-all.paths",
+ "/otel/enduser/roles-mapping-http-perm,/otel/enduser/roles-mapping-http-perm-augmentor");
+ config.put("quarkus.http.auth.roles-mapping.ROLES-ALLOWED-MAPPING-ROLE", "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM");
+ return config;
+ }
+}
diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java
new file mode 100644
index 0000000000000..9e85525ea99d7
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java
@@ -0,0 +1,13 @@
+package io.quarkus.it.opentelemetry.reactive.enduser;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+
+@QuarkusTest
+@TestProfile(LazyAuthEndUserProfile.class)
+public class LazyAuthEndUserEnabledTest extends AbstractEndUserTest {
+ @Override
+ protected boolean isProactiveAuthEnabled() {
+ return false;
+ }
+}
diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java
new file mode 100644
index 0000000000000..f62638221556b
--- /dev/null
+++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java
@@ -0,0 +1,14 @@
+package io.quarkus.it.opentelemetry.reactive.enduser;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class LazyAuthEndUserProfile extends EndUserProfile {
+
+ @Override
+ public Map getConfigOverrides() {
+ var config = new HashMap<>(super.getConfigOverrides());
+ config.put("quarkus.http.auth.proactive", "false");
+ return config;
+ }
+}
diff --git a/integration-tests/opentelemetry/pom.xml b/integration-tests/opentelemetry/pom.xml
index 903728882aee0..d3ac1983b7125 100644
--- a/integration-tests/opentelemetry/pom.xml
+++ b/integration-tests/opentelemetry/pom.xml
@@ -44,7 +44,7 @@
io.quarkus
- quarkus-security
+ quarkus-elytron-security-properties-file
@@ -59,11 +59,6 @@
quarkus-junit5
test
-
- io.quarkus
- quarkus-test-security
- test
-
io.rest-assured
rest-assured
@@ -130,7 +125,7 @@
io.quarkus
- quarkus-security-deployment
+ quarkus-elytron-security-properties-file-deployment
${project.version}
pom
test
diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java
new file mode 100644
index 0000000000000..e5fe326b65d68
--- /dev/null
+++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java
@@ -0,0 +1,43 @@
+package io.quarkus.it.opentelemetry;
+
+import java.util.Map;
+
+import jakarta.inject.Singleton;
+
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.SecurityIdentityAugmentor;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
+import io.smallrye.mutiny.Uni;
+
+@Singleton
+public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
+ @Override
+ public Uni augment(SecurityIdentity securityIdentity,
+ AuthenticationRequestContext authenticationRequestContext) {
+ return augment(securityIdentity, authenticationRequestContext, Map.of());
+ }
+
+ @Override
+ public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context,
+ Map attributes) {
+ var routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
+ if (routingContext != null) {
+ var augmentorScenario = routingContext.normalizedPath().contains("-augmentor");
+ var configRolesMappingScenario = routingContext.normalizedPath().contains("roles-mapping-http-perm");
+ if (augmentorScenario || configRolesMappingScenario) {
+ var builder = QuarkusSecurityIdentity.builder(identity);
+ if (augmentorScenario) {
+ builder.addRole("AUGMENTOR");
+ }
+ if (configRolesMappingScenario) {
+ // this role is supposed to be re-mapped by HTTP roles mapping (not path-specific)
+ builder.addRole("ROLES-ALLOWED-MAPPING-ROLE");
+ }
+ return Uni.createFrom().item(builder.build());
+ }
+ }
+ return Uni.createFrom().item(identity);
+ }
+}
diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java
new file mode 100644
index 0000000000000..e06ff171b8dfd
--- /dev/null
+++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java
@@ -0,0 +1,112 @@
+package io.quarkus.it.opentelemetry;
+
+import jakarta.annotation.security.PermitAll;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.semconv.SemanticAttributes;
+
+@Path("/otel/enduser")
+public class EndUserResource {
+
+ @Inject
+ Tracer tracer;
+
+ @Path("/no-authorization")
+ @GET
+ public String noAuthorization() {
+ return "/no-authorization";
+ }
+
+ @RolesAllowed("WRITER")
+ @Path("/roles-allowed-only-writer-role")
+ @GET
+ public String rolesAllowedOnlyWriterRole() {
+ return "/roles-allowed-only-writer-role";
+ }
+
+ @PermitAll
+ @Path("/permit-all-only")
+ @GET
+ public String permitAllOnly() {
+ return "/permit-all-only";
+ }
+
+ @Path("/no-authorization-augmentor")
+ @GET
+ public String noAuthorizationAugmentor() {
+ return "/no-authorization-augmentor";
+ }
+
+ @RolesAllowed("AUGMENTOR")
+ @Path("/roles-allowed-only-augmentor-role")
+ @GET
+ public String rolesAllowedOnlyAugmentorRole() {
+ return "/roles-allowed-only-augmentor-role";
+ }
+
+ @PermitAll
+ @Path("/permit-all-only-augmentor")
+ @GET
+ public String permitAllOnlyAugmentor() {
+ return "/permit-all-only-augmentor";
+ }
+
+ @RolesAllowed("WRITER-HTTP-PERM")
+ @Path("/roles-allowed-writer-http-perm-role")
+ @GET
+ public String rolesAllowedHttpPermWriterHttpPermRole() {
+ return "/roles-allowed-writer-http-perm-role";
+ }
+
+ @PermitAll
+ @Path("/roles-mapping-http-perm")
+ @GET
+ public String permitAllAnnotationConfigRolesMappingPermitAllHttpPerm() {
+ return "/roles-mapping-http-perm";
+ }
+
+ @RolesAllowed("HTTP-PERM-AUGMENTOR")
+ @Path("/roles-allowed-http-perm-augmentor-role")
+ @GET
+ public String rolesAllowedHttpPermHttpAugmentorPermRole() {
+ return "/roles-allowed-http-perm-augmentor-role";
+ }
+
+ @PermitAll
+ @Path("/roles-mapping-http-perm-augmentor")
+ @GET
+ public String permitAllAnnotationConfigRolesMappingPermitAllHttpPermAugmentor() {
+ return "/roles-mapping-http-perm-augmentor";
+ }
+
+ @Path("/jax-rs-http-perm")
+ @GET
+ public String jaxRsHttpPermOnly() {
+ return "/jax-rs-http-perm";
+ }
+
+ @RolesAllowed("READER")
+ @Path("/jax-rs-http-perm-annotation-reader-role")
+ @GET
+ public String jaxRsHttpPermRolesAllowedReaderRole() {
+ return "/jax-rs-http-perm-annotation-reader-role";
+ }
+
+ @RolesAllowed("READER")
+ @Path("/custom-span-reader-role")
+ @GET
+ public String customSpanReaderRole() {
+ var span = tracer.spanBuilder("custom-span").startSpan();
+ try (var ignored = span.makeCurrent()) {
+ span.setAttribute("custom_attribute", "custom-value");
+ span.setAttribute(SemanticAttributes.HTTP_TARGET, "custom-path");
+ } finally {
+ span.end();
+ }
+ return "/custom-span-reader-role";
+ }
+}
diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties
index 054108ad58ab8..514f563c31933 100644
--- a/integration-tests/opentelemetry/src/main/resources/application.properties
+++ b/integration-tests/opentelemetry/src/main/resources/application.properties
@@ -8,3 +8,11 @@ quarkus.otel.bsp.export.timeout=5s
pingpong/mp-rest/url=${test.url}
simple/mp-rest/url=${test.url}
+
+quarkus.security.users.embedded.roles.stuart=READER,WRITER
+quarkus.security.users.embedded.roles.scott=READER
+quarkus.security.users.embedded.users.stuart=writer
+quarkus.security.users.embedded.users.scott=reader
+quarkus.security.users.embedded.plain-text=true
+quarkus.security.users.embedded.enabled=true
+quarkus.http.auth.basic=true
diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java
new file mode 100644
index 0000000000000..60e70e07dc77a
--- /dev/null
+++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java
@@ -0,0 +1,237 @@
+package io.quarkus.it.opentelemetry;
+
+import static io.restassured.RestAssured.get;
+import static io.restassured.RestAssured.given;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.awaitility.Awaitility;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.opentelemetry.semconv.SemanticAttributes;
+import io.restassured.common.mapper.TypeRef;
+
+public abstract class AbstractEndUserTest {
+
+ private static final String HTTP_PERM_AUGMENTOR = "HTTP-PERM-AUGMENTOR";
+ private static final String END_USER_ID_ATTR = "attr_" + SemanticAttributes.ENDUSER_ID.getKey();
+ private static final String END_USER_ROLE_ATTR = "attr_" + SemanticAttributes.ENDUSER_ROLE.getKey();
+ protected static final String READER_ROLE = "READER";
+ protected static final String WRITER_ROLE = "WRITER";
+ protected static final String AUGMENTOR_ROLE = "AUGMENTOR";
+
+ /**
+ * This is 'ROLES-ALLOWED-MAPPING-ROLE' role granted to the SecureIdentity by augmentor and
+ * remapped to 'ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM' role which allows to verify that the
+ * 'quarkus.http.auth.roles-mapping' config-level roles mapping is reflected in the End User attributes.
+ */
+ protected static final String HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE = "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM";
+
+ @BeforeEach
+ @AfterEach
+ public void reset() {
+ given().get("/reset").then().statusCode(HTTP_OK);
+ await().atMost(5, SECONDS).until(() -> getSpans().isEmpty());
+ }
+
+ @Test
+ public void testAttributesWhenNoAuthorizationInPlace() {
+ assertEndUserAttributes("/no-authorization", isProactiveAuthEnabled(), User.SCOTT, null);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationOnlyUnauthorized() {
+ assertEndUserAttributes("/roles-allowed-only-writer-role", true, User.SCOTT, WRITER_ROLE);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationOnlyAuthorized() {
+ assertEndUserAttributes("/roles-allowed-only-writer-role", true, User.STUART, WRITER_ROLE);
+ }
+
+ @Test
+ public void testWhenPermitAllOnly() {
+ assertEndUserAttributes("/permit-all-only", isProactiveAuthEnabled(), User.STUART, null);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationOnlyAuthorizedAugmentor() {
+ assertEndUserAttributes("/roles-allowed-only-augmentor-role", true, User.SCOTT, AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testWhenPermitAllOnlyAugmentor() {
+ assertEndUserAttributes("/permit-all-only-augmentor", isProactiveAuthEnabled(), User.STUART, AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testAttributesWhenNoAuthorizationInPlaceAugmentor() {
+ assertEndUserAttributes("/no-authorization-augmentor", isProactiveAuthEnabled(), User.SCOTT,
+ AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testWhenConfigRolesMappingAndHttpPermAugmentor() {
+ assertEndUserAttributes("/roles-mapping-http-perm-augmentor", isProactiveAuthEnabled(), User.STUART,
+ null, HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE, AUGMENTOR_ROLE);
+ }
+
+ @Test
+ public void testWhenConfigRolesMappingHttpPerm() {
+ assertEndUserAttributes("/roles-mapping-http-perm", isProactiveAuthEnabled(), User.STUART, null,
+ HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE);
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationHttpPermUnauthorized() {
+ assertEndUserAttributes("/roles-allowed-writer-http-perm-role", true, User.SCOTT, WRITER_ROLE, "AUTHZ-FAILURE-ROLE");
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationHttpPermAuthorized() {
+ assertEndUserAttributes("/roles-allowed-writer-http-perm-role", true, User.STUART, WRITER_ROLE,
+ WRITER_ROLE + "-HTTP-PERM");
+ }
+
+ @Test
+ public void testWhenRolesAllowedAnnotationHttpPermAuthorizedAugmentor() {
+ assertEndUserAttributes("/roles-allowed-http-perm-augmentor-role", true, User.STUART, AUGMENTOR_ROLE,
+ HTTP_PERM_AUGMENTOR);
+ }
+
+ @Test
+ public void testJaxRsHttpPermOnlyAuthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm", true, User.STUART, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testJaxRsHttpPermOnlyUnauthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm", true, User.SCOTT, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testJaxRsHttpPermAndRolesAllowedAnnotationAuthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm-annotation-reader-role", true, User.STUART, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testJaxRsHttpPermAndRolesAllowedAnnotationUnauthorized() {
+ assertEndUserAttributes("/jax-rs-http-perm-annotation-reader-role", true, User.SCOTT, WRITER_ROLE, READER_ROLE);
+ }
+
+ @Test
+ public void testCustomSpanContainsEndUserAttributes() {
+ var path = "/custom-span-reader-role";
+ assertEndUserAttributes(path, true, User.SCOTT, READER_ROLE);
+
+ // assert custom span also contains end user attributes
+ var spanData = waitForSpanWithPath("custom-path");
+ assertEquals("custom-value", spanData.get("attr_custom_attribute"), spanData.toString());
+ assertEquals(User.SCOTT.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString());
+ String role = (String) spanData.get(END_USER_ROLE_ATTR);
+ assertNotNull(role, spanData.toString());
+ assertTrue(role.contains("READER"), spanData.toString());
+ }
+
+ protected void assertEndUserAttributes(String subPath, boolean expectEndUserAttrs, User requestUser, String requiredRole,
+ String... extraRoles) {
+ var path = "/otel/enduser" + subPath;
+ var response = given()
+ .when()
+ .auth().preemptive().basic(requestUser.userName(), requestUser.password)
+ .get(path)
+ .then();
+
+ boolean augmentorScenario = AUGMENTOR_ROLE.equals(requiredRole);
+ boolean accessGranted = augmentorScenario || requiredRole == null || requestUser.roles.contains(requiredRole);
+ if (accessGranted) {
+ response.statusCode(200).body(is(subPath));
+ } else {
+ response.statusCode(403);
+ }
+
+ var spanData = waitForSpanWithPath(path);
+ if (expectEndUserAttrs) {
+ assertEquals(requestUser.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString());
+ String role = (String) spanData.get(END_USER_ROLE_ATTR);
+ assertNotNull(role, spanData.toString());
+
+ if (augmentorScenario) {
+ assertTrue(role.contains(AUGMENTOR_ROLE));
+ assertTrue(role.contains(AUGMENTOR_ROLE));
+ }
+
+ assertTrue(requestUser.roles.stream().allMatch(role::contains), spanData.toString());
+
+ for (String extraRole : extraRoles) {
+ assertTrue(role.contains(extraRole), spanData.toString());
+ }
+ } else {
+ assertNull(spanData.get(END_USER_ID_ATTR), spanData.toString());
+ assertNull(spanData.get(END_USER_ROLE_ATTR), spanData.toString());
+ }
+ }
+
+ private List