From 238c37bc82438df1e2175d03147ddbb8e6b234b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 8 May 2024 18:50:31 +0200 Subject: [PATCH] Support OpenTelemetry End User attributes added as Span attributes --- docs/src/main/asciidoc/opentelemetry.adoc | 26 ++ .../deployment/tracing/TracerProcessor.java | 40 ++ .../config/build/TracesBuildConfig.java | 12 + .../security/EndUserSpanProcessor.java | 35 ++ .../tracing/security/SecurityEventUtil.java | 124 ++++- .../CustomSecurityIdentityAugmentor.java | 43 ++ .../reactive/EndUserResource.java | 112 +++++ .../reactive/enduser/AbstractEndUserTest.java | 429 +++++++++++++++++ .../enduser/EagerAuthEndUserEnabledTest.java | 15 + .../reactive/enduser/EndUserProfile.java | 33 ++ .../enduser/LazyAuthEndUserEnabledTest.java | 13 + .../enduser/LazyAuthEndUserProfile.java | 14 + integration-tests/opentelemetry/pom.xml | 9 +- .../CustomSecurityIdentityAugmentor.java | 43 ++ .../it/opentelemetry/EndUserResource.java | 112 +++++ .../src/main/resources/application.properties | 8 + .../it/opentelemetry/AbstractEndUserTest.java | 435 ++++++++++++++++++ .../EagerAuthEndUserEnabledTest.java | 16 + .../LazyAuthEndUserEnabledTest.java | 14 + .../it/opentelemetry/OpenTelemetryTest.java | 31 ++ .../it/opentelemetry/util/EndUserProfile.java | 33 ++ .../util/LazyAuthEndUserProfile.java | 14 + 22 files changed, 1599 insertions(+), 12 deletions(-) create mode 100644 extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java create mode 100644 integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java create mode 100644 integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java create mode 100644 integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java create mode 100644 integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java create mode 100644 integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java create mode 100644 integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java create mode 100644 integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java create mode 100644 integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java create mode 100644 integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java create mode 100644 integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java create mode 100644 integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java create mode 100644 integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java create mode 100644 integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java create mode 100644 integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index 5462114043b5b..6f08bda91c7ab 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -398,6 +398,32 @@ public class CustomConfiguration { } ---- +==== End User attributes + +When enabled, Quarkus adds OpenTelemetry End User attributes as Span attributes. +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]. + +The attributes are only added when authentication has already happened on a best-efforts basis. +Whether the End User attributes are added as Span attributes depends on authentication and authorization configuration of your Quarkus application. +If you create custom Spans prior to the authentication, Quarkus cannot add the End User attributes to them. +Quarkus is only able to add the attributes to the Span that is current after the authentication has been finished. +Another important consideration regarding custom Spans is active CDI request context that is used to propagate Quarkus `SecurityIdentity`. +In principle, Quarkus is able to add the End User attributes when the CDI request context has been activated for you before the custom Spans are created. + +[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. +A good way to determine whether proactive authentication should be enabled in your Quarkus application is to read the Quarkus xref:security-proactive-authentication.adoc[Proactive authentication] guide. + +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..4f67ed23eacdc 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; @@ -49,6 +52,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 +202,28 @@ void registerSecurityEventObserver(Capabilities capabilities, OTelBuildConfig bu } } + @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 +258,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..841ab567c1b2d --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java @@ -0,0 +1,429 @@ +package io.quarkus.it.opentelemetry.reactive.enduser; + +import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans; +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.assertFalse; +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 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.response.ValidatableResponse; + +public abstract class AbstractEndUserTest { + + private static final String HTTP_PERM_AUGMENTOR_ROLE = "HTTP-PERM-AUGMENTOR"; + 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 READER_ROLE = "READER"; + private static final String WRITER_ROLE = "WRITER"; + private static final String WRITER_HTTP_PERM_ROLE = "WRITER-HTTP-PERM"; + private static final String AUTH_FAILURE_ROLE = "AUTHZ-FAILURE-ROLE"; + private 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. + */ + private 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() { + var subPath = "/no-authorization"; + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyUnauthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnly() { + var subPath = "/permit-all-only"; + // request to endpoint with @PermitAll annotation + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorizedAugmentor() { + var subPath = "/roles-allowed-only-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("AUGMENTOR") and no other authorization is in place + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnlyAugmentor() { + var subPath = "/permit-all-only-augmentor"; + // the endpoint is annotated with @PermitAll and no authorization is in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testAttributesWhenNoAuthorizationInPlaceAugmentor() { + var subPath = "/no-authorization-augmentor"; + // there is no authorization in place, therefore authentication happnes on demand + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingAndHttpPermAugmentor() { + var subPath = "/roles-mapping-http-perm-augmentor"; + // the endpoint is annotated with @PermitAll, HTTP permission 'permit-all' is in place; auth happens on demand + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingHttpPerm() { + var subPath = "/roles-mapping-http-perm"; + // request endpoint with both 'permit-all' HTTP permission and @PermitAll annotation + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermUnauthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + assertFalse(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'WRITER-HTTP-PERM' role is remapped from 'WRITER' role by HTTP permission roles policy + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorizedAugmentor() { + var subPath = "/roles-allowed-http-perm-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("HTTP-PERM-AUGMENTOR") + // and role 'HTTP-PERM-AUGMENTOR' is mapped by HTTP perm roles policy from the 'AUGMENTOR' role + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_AUGMENTOR_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyAuthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyUnauthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationAuthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationUnauthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testCustomSpanContainsEndUserAttributes() { + var subPath = "/custom-span-reader-role"; + // the endpoint is annotated with @RolesAllowed("READER") and no other authorization is in place + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + + // assert custom span also contains end user attributes + spanData = waitForSpanWithPath("custom-path"); + assertEquals("custom-value", spanData.get("custom_attribute"), spanData.toString()); + + assertEndUserId(User.SCOTT, spanData); + + roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + protected abstract boolean isProactiveAuthEnabled(); + + enum User { + + SCOTT("reader"), // READER_ROLE + STUART("writer"); // READER_ROLE, WRITER_ROLE + + private final String password; + + User(String password) { + this.password = password; + } + + private String userName() { + return this.toString().toLowerCase(); + } + } + + private static void assertEndUserId(User requestUser, Map spanData) { + assertEquals(requestUser.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString()); + } + + private static void assertNoEndUserAttributes(Map spanData) { + assertNull(spanData.get(END_USER_ID_ATTR), spanData.toString()); + assertNull(spanData.get(END_USER_ROLE_ATTR), spanData.toString()); + } + + private static String getRolesAttribute(Map spanData) { + var roles = (String) spanData.get(END_USER_ROLE_ATTR); + assertNotNull(roles, spanData.toString()); + return roles; + } + + private static ValidatableResponse request(String subPath, User requestUser) { + return given() + .when() + .auth().preemptive().basic(requestUser.userName(), requestUser.password) + .get(createResourcePath(subPath)) + .then(); + } + + private static String createResourcePath(String subPath) { + return "/otel/enduser" + subPath; + } + + @SuppressWarnings("unchecked") + private static Map getSpanByPath(final String path) { + return getSpans() + .stream() + .map(m -> (Map) m.get("attributes")) + .filter(m -> path.equals(m.get(SemanticAttributes.HTTP_TARGET.getKey()))) + .findFirst() + .orElse(Map.of()); + } + + private static Map waitForSpanWithSubPath(final String subPath) { + return waitForSpanWithPath(createResourcePath(subPath)); + } + + private static 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 + " ; " + getSpans()); + } + }); + return getSpanByPath(path); + } +} 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..afb0ebc55ee05 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java @@ -0,0 +1,435 @@ +package io.quarkus.it.opentelemetry; + +import static io.quarkus.it.opentelemetry.AbstractEndUserTest.User.SCOTT; +import static io.quarkus.it.opentelemetry.AbstractEndUserTest.User.STUART; +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.assertFalse; +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 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; +import io.restassured.response.ValidatableResponse; + +public abstract class AbstractEndUserTest { + + private static final String HTTP_PERM_AUGMENTOR_ROLE = "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(); + private static final String READER_ROLE = "READER"; + private static final String WRITER_ROLE = "WRITER"; + private static final String WRITER_HTTP_PERM_ROLE = "WRITER-HTTP-PERM"; + private static final String AUTH_FAILURE_ROLE = "AUTHZ-FAILURE-ROLE"; + private 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. + */ + private 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() { + var subPath = "/no-authorization"; + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyUnauthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnly() { + var subPath = "/permit-all-only"; + // request to endpoint with @PermitAll annotation + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorizedAugmentor() { + var subPath = "/roles-allowed-only-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("AUGMENTOR") and no other authorization is in place + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnlyAugmentor() { + var subPath = "/permit-all-only-augmentor"; + // the endpoint is annotated with @PermitAll and no authorization is in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testAttributesWhenNoAuthorizationInPlaceAugmentor() { + var subPath = "/no-authorization-augmentor"; + // there is no authorization in place, therefore authentication happnes on demand + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingAndHttpPermAugmentor() { + var subPath = "/roles-mapping-http-perm-augmentor"; + // the endpoint is annotated with @PermitAll, HTTP permission 'permit-all' is in place; auth happens on demand + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingHttpPerm() { + var subPath = "/roles-mapping-http-perm"; + // request endpoint with both 'permit-all' HTTP permission and @PermitAll annotation + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermUnauthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + assertFalse(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'WRITER-HTTP-PERM' role is remapped from 'WRITER' role by HTTP permission roles policy + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorizedAugmentor() { + var subPath = "/roles-allowed-http-perm-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("HTTP-PERM-AUGMENTOR") + // and role 'HTTP-PERM-AUGMENTOR' is mapped by HTTP perm roles policy from the 'AUGMENTOR' role + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_AUGMENTOR_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyAuthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyUnauthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationAuthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationUnauthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testCustomSpanContainsEndUserAttributes() { + var subPath = "/custom-span-reader-role"; + // the endpoint is annotated with @RolesAllowed("READER") and no other authorization is in place + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + + // assert custom span also contains end user attributes + spanData = waitForSpanWithPath("custom-path"); + assertEquals("custom-value", spanData.get("attr_custom_attribute"), spanData.toString()); + + assertEndUserId(SCOTT, spanData); + + roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + protected abstract boolean isProactiveAuthEnabled(); + + enum User { + + SCOTT("reader"), // READER_ROLE + STUART("writer"); // READER_ROLE, WRITER_ROLE + + private final String password; + + User(String password) { + this.password = password; + } + + private String userName() { + return this.toString().toLowerCase(); + } + } + + private static void assertEndUserId(User requestUser, Map spanData) { + assertEquals(requestUser.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString()); + } + + private static void assertNoEndUserAttributes(Map spanData) { + assertNull(spanData.get(END_USER_ID_ATTR), spanData.toString()); + assertNull(spanData.get(END_USER_ROLE_ATTR), spanData.toString()); + } + + private static String getRolesAttribute(Map spanData) { + var roles = (String) spanData.get(END_USER_ROLE_ATTR); + assertNotNull(roles, spanData.toString()); + return roles; + } + + private static ValidatableResponse request(String subPath, User requestUser) { + return given() + .when() + .auth().preemptive().basic(requestUser.userName(), requestUser.password) + .get(createResourcePath(subPath)) + .then(); + } + + private static String createResourcePath(String subPath) { + return "/otel/enduser" + subPath; + } + + private static List> getSpans() { + return get("/export").body().as(new TypeRef<>() { + }); + } + + private static Map getSpanByPath(final String path) { + return getSpans() + .stream() + .filter(m -> path.equals(m.get("attr_" + SemanticAttributes.HTTP_TARGET.getKey()))) + .findFirst() + .orElse(Map.of()); + } + + private static Map waitForSpanWithSubPath(final String subPath) { + return waitForSpanWithPath(createResourcePath(subPath)); + } + + private static 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 + " ; " + getSpans()); + } + }); + return getSpanByPath(path); + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java new file mode 100644 index 0000000000000..37d7c40b319b1 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry; + +import io.quarkus.it.opentelemetry.util.EndUserProfile; +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/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java new file mode 100644 index 0000000000000..82f4d329840ef --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java @@ -0,0 +1,14 @@ +package io.quarkus.it.opentelemetry; + +import io.quarkus.it.opentelemetry.util.LazyAuthEndUserProfile; +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/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java index a8df12fd2304b..8b9173127ea6d 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java @@ -27,7 +27,9 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,6 +40,7 @@ import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.it.opentelemetry.util.SocketClient; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; @@ -705,6 +708,34 @@ void testWrongHTTPVersion() { await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 1); } + /** + * Test no End User attributes are added when the feature is disabled. + */ + @Test + public void testNoEndUserAttributes() { + RestAssured + .given() + .auth().preemptive().basic("stuart", "writer") + .get("/otel/enduser/roles-allowed-only-writer-role") + .then() + .statusCode(200) + .body(Matchers.is("/roles-allowed-only-writer-role")); + RestAssured + .given() + .auth().preemptive().basic("scott", "reader") + .get("/otel/enduser/roles-allowed-only-writer-role") + .then() + .statusCode(403); + await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() > 1); + List> spans = getSpans(); + Assertions.assertTrue(spans + .stream() + .flatMap(m -> m.entrySet().stream()) + .filter(e -> ("attr_" + SemanticAttributes.ENDUSER_ID.getKey()).equals(e.getKey()) + || ("attr_" + SemanticAttributes.ENDUSER_ROLE.getKey()).equals(e.getKey())) + .findAny().isEmpty()); + } + private void verifyResource(Map spanData) { assertEquals("opentelemetry-integration-test", spanData.get("resource_service.name")); assertEquals("999-SNAPSHOT", spanData.get("resource_service.version")); diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java new file mode 100644 index 0000000000000..0ca0a38cdd01f --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java @@ -0,0 +1,33 @@ +package io.quarkus.it.opentelemetry.util; + +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/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java new file mode 100644 index 0000000000000..471bce77e748b --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.it.opentelemetry.util; + +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; + } +}