Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export security events as OpenTelemetry Span Events #39207

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -609,8 +609,18 @@
----

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.

Check warning on line 612 in docs/src/main/asciidoc/opentelemetry.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Quarkus Security Events'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Quarkus Security Events'.", "location": {"path": "docs/src/main/asciidoc/opentelemetry.adoc", "range": {"start": {"line": 612, "column": 46}}}, "severity": "INFO"}

=== 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
Loading