Skip to content

Commit

Permalink
Merge pull request quarkusio#39207 from michalvavrik/feature/security…
Browse files Browse the repository at this point in the history
…-events-as-otel-span-events

Export security events as OpenTelemetry Span Events
  • Loading branch information
brunobat authored Mar 7, 2024
2 parents 387c769 + 3d08150 commit dc10a9d
Show file tree
Hide file tree
Showing 10 changed files with 466 additions and 0 deletions.
10 changes: 10 additions & 0 deletions docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions extensions/opentelemetry/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@
<artifactId>quarkus-jdbc-h2-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-deployment</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
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;
import java.util.HashSet;
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;
import org.jboss.jandex.DotName;
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;
Expand All @@ -23,24 +29,33 @@
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;
import io.quarkus.deployment.annotations.ExecutionTime;
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());
Expand Down Expand Up @@ -162,4 +177,59 @@ void staticInitSetup(
dropNonApplicationUris.getDropNames(),
dropStaticResources.getDropNames());
}

@BuildStep(onlyIf = SecurityEventsEnabled.class)
void registerSecurityEventObserver(Capabilities capabilities, OTelBuildConfig buildConfig,
ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
BuildProducer<ObserverConfiguratorBuildItem> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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> authenticationSuccessEvent;

@Inject
Event<AuthenticationFailureEvent> authenticationFailureEvent;

@Inject
Event<AuthorizationFailureEvent> authorizationFailureEvent;

@Inject
Event<AuthorizationSuccessEvent> authorizationSuccessEvent;

@Inject
Event<CustomSecurityEvent> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SecurityEventType> 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<? extends SecurityEvent> observedType;

SecurityEventType(Class<? extends SecurityEvent> observedType) {
this.observedType = observedType;
}

public Class<? extends SecurityEvent> getObservedType() {
return observedType;
}
}
}
}
Loading

0 comments on commit dc10a9d

Please sign in to comment.