From 3d08150b09ed4cda6b38a80744f5379661099288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 6 Mar 2024 11:03:02 +0100 Subject: [PATCH] Export security events as OpenTelemetry Span events --- docs/src/main/asciidoc/opentelemetry.adoc | 10 ++ extensions/opentelemetry/deployment/pom.xml | 5 + .../deployment/tracing/TracerProcessor.java | 70 ++++++++ .../OpenTelemetrySpanSecurityEventsTest.java | 102 ++++++++++++ .../runtime/config/build/OTelBuildConfig.java | 69 ++++++++ .../tracing/security/SecurityEventUtil.java | 149 ++++++++++++++++++ .../reactive/ExporterResource.java | 17 ++ .../src/main/resources/application.properties | 2 + .../reactive/OpenTelemetryReactiveTest.java | 33 ++++ .../it/opentelemetry/reactive/Utils.java | 9 ++ 10 files changed, 466 insertions(+) create mode 100644 extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java create mode 100644 extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index fbb182223cda5..b61c3542cfe75 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -611,6 +611,16 @@ Message out = Message.of(...).withMetadata(tm); The above creates a `TracingMetadata` object we can add to the `Message` being produced, which retrieves the OpenTelemetry `Context` to extract the current span for propagation. +=== Quarkus Security Events + +Quarkus supports exporting of the xref:security-customization.adoc#observe-security-events[Security events] as OpenTelemetry Span events. + +[source,application.properties] +---- +quarkus.otel.security-events.enabled=true <1> +---- +<1> Export Quarkus Security events as OpenTelemetry Span events. + == Exporters === Default diff --git a/extensions/opentelemetry/deployment/pom.xml b/extensions/opentelemetry/deployment/pom.xml index bccf605a4dfb5..e943ada5958ea 100644 --- a/extensions/opentelemetry/deployment/pom.xml +++ b/extensions/opentelemetry/deployment/pom.xml @@ -131,6 +131,11 @@ quarkus-jdbc-h2-deployment test + + io.quarkus + quarkus-security-deployment + test + 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 b07e5891fbd53..14f09b0a37753 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,5 +1,7 @@ package io.quarkus.opentelemetry.deployment.tracing; +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.ALL; + import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -7,6 +9,9 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.BooleanSupplier; + +import jakarta.enterprise.inject.spi.EventContext; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -14,6 +19,7 @@ import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.sdk.resources.Resource; @@ -23,10 +29,13 @@ import io.opentelemetry.sdk.trace.samplers.Sampler; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem; +import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.processor.DotNames; import io.quarkus.builder.Version; import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -34,13 +43,19 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; +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.SecurityEventUtil; import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem; import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; @BuildSteps(onlyIf = TracerEnabled.class) public class TracerProcessor { + private static final Logger LOGGER = Logger.getLogger(TracerProcessor.class.getName()); private static final DotName ID_GENERATOR = DotName.createSimple(IdGenerator.class.getName()); private static final DotName RESOURCE = DotName.createSimple(Resource.class.getName()); private static final DotName SAMPLER = DotName.createSimple(Sampler.class.getName()); @@ -162,4 +177,59 @@ void staticInitSetup( dropNonApplicationUris.getDropNames(), dropStaticResources.getDropNames()); } + + @BuildStep(onlyIf = SecurityEventsEnabled.class) + void registerSecurityEventObserver(Capabilities capabilities, OTelBuildConfig buildConfig, + ObserverRegistrationPhaseBuildItem observerRegistrationPhase, + BuildProducer observerProducer) { + if (capabilities.isPresent(Capability.SECURITY)) { + if (buildConfig.securityEvents().eventTypes().contains(ALL)) { + observerProducer.produce(createEventObserver(observerRegistrationPhase, ALL, "addAllEvents")); + } else { + for (SecurityEventType eventType : buildConfig.securityEvents().eventTypes()) { + observerProducer.produce(createEventObserver(observerRegistrationPhase, eventType, "addEvent")); + } + } + } else { + LOGGER.warn(""" + Exporting of Quarkus Security events as OpenTelemetry Span events is enabled, + but the Quarkus Security is missing. This feature will only work if you add the Quarkus Security extension. + """); + } + } + + private static ObserverConfiguratorBuildItem createEventObserver( + ObserverRegistrationPhaseBuildItem observerRegistrationPhase, SecurityEventType eventType, String utilMethodName) { + return new ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext() + .configure() + .beanClass(DotName.createSimple(TracerProducer.class.getName())) + .observedType(eventType.getObservedType()) + .notify(mc -> { + // Object event = eventContext.getEvent(); + ResultHandle eventContext = mc.getMethodParam(0); + ResultHandle event = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(EventContext.class, "getEvent", Object.class), eventContext); + // Call to SecurityEventUtil#addEvent or SecurityEventUtil#addAllEvents, that is: + // SecurityEventUtil.addAllEvents((SecurityEvent) event) + // SecurityEventUtil.addEvent((AuthenticationSuccessEvent) event) + // Method 'addEvent' is overloaded and accepts SecurityEventType#getObservedType + mc.invokeStaticMethod(MethodDescriptor.ofMethod(SecurityEventUtil.class, utilMethodName, + void.class, eventType.getObservedType()), mc.checkCast(event, eventType.getObservedType())); + mc.returnNull(); + })); + } + + static final class SecurityEventsEnabled implements BooleanSupplier { + + private final boolean enabled; + + SecurityEventsEnabled(OTelBuildConfig config) { + this.enabled = config.securityEvents().enabled(); + } + + @Override + public boolean getAsBoolean() { + return enabled; + } + } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java new file mode 100644 index 0000000000000..616786d7ee668 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java @@ -0,0 +1,102 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.AttributeKey; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; +import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil; +import io.quarkus.security.spi.runtime.AbstractSecurityEvent; +import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; +import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetrySpanSecurityEventsTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, EventsResource.class, + CustomSecurityEvent.class) + .addAsResource(new StringAsset(""" + quarkus.otel.security-events.enabled=true + quarkus.otel.security-events.event-types=AUTHENTICATION_SUCCESS,AUTHORIZATION_SUCCESS,OTHER + """), "application.properties")); + + @Inject + TestSpanExporter spanExporter; + + @Test + public void testSecurityEventTypes() { + spanExporter.assertSpanCount(0); + RestAssured.post("/events"); + spanExporter.assertSpanCount(1); + var events = spanExporter.getFinishedSpanItems(1).get(0).getEvents(); + assertEquals(3, events.size()); + assertTrue(events.stream().anyMatch(ed -> SecurityEventUtil.AUTHN_SUCCESS_EVENT_NAME.equals(ed.getName()))); + assertTrue(events.stream().anyMatch(ed -> SecurityEventUtil.AUTHZ_SUCCESS_EVENT_NAME.equals(ed.getName()))); + assertTrue(events.stream().anyMatch(ed -> SecurityEventUtil.OTHER_EVENT_NAME.equals(ed.getName()))); + var event = events.stream().filter(s -> SecurityEventUtil.OTHER_EVENT_NAME.equals(s.getName())).findFirst() + .orElse(null); + assertNotNull(event); + assertEquals(1, event.getAttributes().size()); + assertEquals(CustomSecurityEvent.CUSTOM_VALUE, event.getAttributes().get(AttributeKey + .stringKey(SecurityEventUtil.QUARKUS_SECURITY_OTHER_EVENTS_NAMESPACE + CustomSecurityEvent.CUSTOM_KEY))); + } + + public static class CustomSecurityEvent extends AbstractSecurityEvent { + private static final String CUSTOM_KEY = "custom-key"; + private static final String CUSTOM_VALUE = "custom-value"; + + protected CustomSecurityEvent() { + super(null, Map.of(CUSTOM_KEY, CUSTOM_VALUE)); + } + } + + @Path("/events") + public static class EventsResource { + + @Inject + Event authenticationSuccessEvent; + + @Inject + Event authenticationFailureEvent; + + @Inject + Event authorizationFailureEvent; + + @Inject + Event authorizationSuccessEvent; + + @Inject + Event customEvent; + + @POST + public void fire() { + authenticationSuccessEvent.fire(new AuthenticationSuccessEvent(null, null)); + authenticationFailureEvent.fire(new AuthenticationFailureEvent(new ForbiddenException(), null)); + authorizationSuccessEvent.fire(new AuthorizationSuccessEvent(null, null)); + authorizationFailureEvent.fire(new AuthorizationFailureEvent(null, new ForbiddenException(), "endpoint")); + customEvent.fire(new CustomSecurityEvent()); + } + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java index 679cf07f40d4c..6a7a5aa09928b 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java @@ -5,8 +5,14 @@ import java.util.List; +import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; +import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; +import io.quarkus.security.spi.runtime.SecurityEvent; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; @@ -67,4 +73,67 @@ public interface OTelBuildConfig { * Enable/disable instrumentation for specific technologies. */ InstrumentBuildTimeConfig instrument(); + + /** + * Allows to export Quarkus security events as the OpenTelemetry Span events. + */ + SecurityEvents securityEvents(); + + /** + * Quarkus security events exported as the OpenTelemetry Span events. + */ + @ConfigGroup + interface SecurityEvents { + /** + * Whether exporting of the security events is enabled. + */ + @WithDefault("false") + boolean enabled(); + + /** + * Selects security event types. + */ + @WithDefault("ALL") + List eventTypes(); + + /** + * Security event type. + */ + enum SecurityEventType { + /** + * All the security events. + */ + ALL(SecurityEvent.class), + /** + * Authentication success event. + */ + AUTHENTICATION_SUCCESS(AuthenticationSuccessEvent.class), + /** + * Authentication failure event. + */ + AUTHENTICATION_FAILURE(AuthenticationFailureEvent.class), + /** + * Authorization success event. + */ + AUTHORIZATION_SUCCESS(AuthorizationSuccessEvent.class), + /** + * Authorization failure event. + */ + AUTHORIZATION_FAILURE(AuthorizationFailureEvent.class), + /** + * Any other security event. For example the OpenId Connect security event belongs here. + */ + OTHER(SecurityEvent.class); + + private final Class observedType; + + SecurityEventType(Class observedType) { + this.observedType = observedType; + } + + public Class getObservedType() { + return observedType; + } + } + } } 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 new file mode 100644 index 0000000000000..e162fb57b89e5 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java @@ -0,0 +1,149 @@ +package io.quarkus.opentelemetry.runtime.tracing.security; + +import static io.quarkus.security.spi.runtime.AuthenticationFailureEvent.AUTHENTICATION_FAILURE_KEY; +import static io.quarkus.security.spi.runtime.AuthorizationFailureEvent.AUTHORIZATION_FAILURE_KEY; + +import java.time.Instant; +import java.util.function.BiConsumer; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.quarkus.arc.Arc; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; +import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; +import io.quarkus.security.spi.runtime.SecurityEvent; + +/** + * 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. + */ +public final class SecurityEventUtil { + public static final String QUARKUS_SECURITY_NAMESPACE = "quarkus.security."; + public static final String AUTHN_SUCCESS_EVENT_NAME = QUARKUS_SECURITY_NAMESPACE + "authentication.success"; + public static final String AUTHN_FAILURE_EVENT_NAME = QUARKUS_SECURITY_NAMESPACE + "authentication.failure"; + public static final String AUTHZ_SUCCESS_EVENT_NAME = QUARKUS_SECURITY_NAMESPACE + "authorization.success"; + public static final String AUTHZ_FAILURE_EVENT_NAME = QUARKUS_SECURITY_NAMESPACE + "authorization.failure"; + public static final String OTHER_EVENT_NAME = QUARKUS_SECURITY_NAMESPACE + "other"; + public static final String SECURITY_IDENTITY_PRINCIPAL = QUARKUS_SECURITY_NAMESPACE + "identity.principal"; + public static final String SECURITY_IDENTITY_IS_ANONYMOUS = QUARKUS_SECURITY_NAMESPACE + "identity.anonymous"; + public static final String QUARKUS_SECURITY_OTHER_EVENTS_NAMESPACE = QUARKUS_SECURITY_NAMESPACE + "other."; + public static final String FAILURE_NAME = QUARKUS_SECURITY_NAMESPACE + "failure.name"; + public static final String AUTHORIZATION_CONTEXT = QUARKUS_SECURITY_NAMESPACE + "authorization.context"; + + private SecurityEventUtil() { + // UTIL CLASS + } + + /** + * 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) { + if (event instanceof AuthenticationSuccessEvent e) { + addEvent(e); + } else if (event instanceof AuthenticationFailureEvent e) { + addEvent(e); + } else if (event instanceof AuthorizationSuccessEvent e) { + addEvent(e); + } else if (event instanceof AuthorizationFailureEvent e) { + addEvent(e); + } else { + addOtherEventInternal(event); + } + } + + /** + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + */ + public static void addEvent(AuthenticationSuccessEvent event) { + addEvent(AUTHN_SUCCESS_EVENT_NAME, attributesBuilder(event).build()); + } + + /** + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + */ + public static void addEvent(AuthenticationFailureEvent event) { + addEvent(AUTHN_FAILURE_EVENT_NAME, attributesBuilder(event, AUTHENTICATION_FAILURE_KEY).build()); + } + + /** + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + */ + public static void addEvent(AuthorizationSuccessEvent event) { + addEvent(AUTHZ_SUCCESS_EVENT_NAME, + withAuthorizationContext(event, attributesBuilder(event), AuthorizationSuccessEvent.AUTHORIZATION_CONTEXT)); + } + + /** + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + */ + public static void addEvent(AuthorizationFailureEvent event) { + addEvent(AUTHZ_FAILURE_EVENT_NAME, withAuthorizationContext(event, attributesBuilder(event, AUTHORIZATION_FAILURE_KEY), + AuthorizationFailureEvent.AUTHORIZATION_CONTEXT_KEY)); + } + + /** + * 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) { + if (!(event instanceof AuthenticationSuccessEvent || event instanceof AuthenticationFailureEvent + || event instanceof AuthorizationSuccessEvent || event instanceof AuthorizationFailureEvent)) { + addOtherEventInternal(event); + } + } + + private static void addOtherEventInternal(SecurityEvent event) { + var builder = attributesBuilder(event); + // add all event properties that are string, for example OIDC authentication server URL + event.getEventProperties().forEach(new BiConsumer() { + @Override + public void accept(String key, Object value) { + if (value instanceof String str) { + builder.put(QUARKUS_SECURITY_OTHER_EVENTS_NAMESPACE + key, str); + } + } + }); + addEvent(OTHER_EVENT_NAME, builder.build()); + } + + private static void addEvent(String eventName, Attributes attributes) { + Span span = Arc.container().select(Span.class).get(); + if (span.getSpanContext().isValid()) { + span.addEvent(eventName, attributes, Instant.now()); + } + } + + private static AttributesBuilder attributesBuilder(SecurityEvent event, String failureKey) { + Throwable failure = (Throwable) event.getEventProperties().get(failureKey); + if (failure != null) { + return attributesBuilder(event).put(FAILURE_NAME, failure.getClass().getName()); + } + return attributesBuilder(event); + } + + private static AttributesBuilder attributesBuilder(SecurityEvent event) { + var builder = Attributes.builder(); + + SecurityIdentity identity = event.getSecurityIdentity(); + if (identity != null) { + builder.put(SECURITY_IDENTITY_IS_ANONYMOUS, identity.isAnonymous()); + if (identity.getPrincipal() != null) { + builder.put(SECURITY_IDENTITY_PRINCIPAL, identity.getPrincipal().getName()); + } + } + + return builder; + } + + private static Attributes withAuthorizationContext(SecurityEvent event, AttributesBuilder builder, String contextKey) { + if (event.getEventProperties().containsKey(contextKey)) { + builder.put(AUTHORIZATION_CONTEXT, (String) event.getEventProperties().get(contextKey)); + } + return builder.build(); + } +} diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java index 0d8f6cd3c4956..be1350677b2fc 100644 --- a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java +++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/ExporterResource.java @@ -1,6 +1,8 @@ package io.quarkus.it.opentelemetry.reactive; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; @@ -11,6 +13,8 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.RestQuery; + import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData; @@ -36,6 +40,19 @@ public List export() { .collect(Collectors.toList()); } + @GET + @Path("/export-event-attributes") + public Map exportEventAttributes(@RestQuery String spanName, @RestQuery String eventName) { + return export() + .stream() + .filter(s -> spanName.equals(s.getName())) + .map(SpanData::getEvents) + .flatMap(Collection::stream) + .filter(e -> eventName.equals(e.getName())) + .flatMap(e -> e.getAttributes().asMap().entrySet().stream()) + .collect(Collectors.toMap(e -> e.getKey().getKey(), Map.Entry::getValue)); + } + @GET @Path("/exportExceptionMessages") public List exportExceptionMessages() { diff --git a/integration-tests/opentelemetry-reactive/src/main/resources/application.properties b/integration-tests/opentelemetry-reactive/src/main/resources/application.properties index db811cf7b4cce..0518dd1e503eb 100644 --- a/integration-tests/opentelemetry-reactive/src/main/resources/application.properties +++ b/integration-tests/opentelemetry-reactive/src/main/resources/application.properties @@ -11,3 +11,5 @@ quarkus.security.users.embedded.roles.scott=READER quarkus.security.users.embedded.roles.stuart=READER,WRITER quarkus.http.auth.permission.secured.policy=authenticated quarkus.http.auth.permission.secured.paths=/secured/* + +quarkus.otel.security-events.enabled=true diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java index cdaba582dcb6e..ce769b531cae6 100644 --- a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java @@ -7,6 +7,7 @@ import static io.opentelemetry.semconv.SemanticAttributes.HTTP_URL; import static io.quarkus.it.opentelemetry.reactive.Utils.getExceptionEventData; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanByKindAndParentId; +import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanEventAttrs; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpansByKindAndParentId; import static io.restassured.RestAssured.given; @@ -25,9 +26,12 @@ import java.util.Optional; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil; +import io.quarkus.security.AuthenticationFailedException; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest @@ -208,6 +212,7 @@ public void securedInvalidCredential() { await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); assertThat(getSpans()).singleElement().satisfies(m -> { assertThat(m).extractingByKey("name").isEqualTo("GET /secured/item/{value}"); + assertEvent(m, SecurityEventUtil.AUTHN_FAILURE_EVENT_NAME); }); } @@ -220,6 +225,34 @@ public void securedProperCredentials() { await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); assertThat(getSpans()).singleElement().satisfies(m -> { assertThat(m).extractingByKey("name").isEqualTo("GET /secured/item/{value}"); + assertEvent(m, SecurityEventUtil.AUTHN_SUCCESS_EVENT_NAME, SecurityEventUtil.AUTHZ_SUCCESS_EVENT_NAME); }); } + + private static void assertEvent(Map spanData, String... expectedEventNames) { + String spanName = (String) spanData.get("name"); + var events = (List) spanData.get("events"); + Assertions.assertEquals(expectedEventNames.length, events.size()); + for (String expectedEventName : expectedEventNames) { + boolean foundEvent = events.stream().anyMatch(m -> expectedEventName.equals(((Map) m).get("name"))); + assertTrue(foundEvent, "Span '%s' did not contain event '%s'".formatted(spanName, expectedEventName)); + assertEventAttributes(spanName, expectedEventName); + } + } + + private static void assertEventAttributes(String spanName, String eventName) { + var attrs = getSpanEventAttrs(spanName, eventName); + switch (eventName) { + case SecurityEventUtil.AUTHN_FAILURE_EVENT_NAME: + assertEquals(AuthenticationFailedException.class.getName(), attrs.get(SecurityEventUtil.FAILURE_NAME)); + break; + case SecurityEventUtil.AUTHN_SUCCESS_EVENT_NAME: + case SecurityEventUtil.AUTHZ_SUCCESS_EVENT_NAME: + assertEquals("scott", attrs.get(SecurityEventUtil.SECURITY_IDENTITY_PRINCIPAL)); + assertEquals(Boolean.FALSE, attrs.get(SecurityEventUtil.SECURITY_IDENTITY_IS_ANONYMOUS)); + break; + default: + Assertions.fail("Unknown event name " + eventName); + } + } } diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/Utils.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/Utils.java index 56ef345d5fabf..d50288f840c01 100644 --- a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/Utils.java +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/Utils.java @@ -1,5 +1,6 @@ package io.quarkus.it.opentelemetry.reactive; +import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -21,6 +22,14 @@ public static List> getSpans() { }); } + public static Map getSpanEventAttrs(String spanName, String eventName) { + return given() + .queryParam("spanName", spanName) + .queryParam("eventName", eventName) + .get("/export-event-attributes").body().as(new TypeRef<>() { + }); + } + public static List getExceptionEventData() { return when().get("/exportExceptionMessages").body().as(new TypeRef<>() { });