From ab9f67fbff01ccd4e029b59e8243657e1c945b4c Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 12 Jul 2023 15:32:10 +0300 Subject: [PATCH] Properly populate metrics uri in presence of auth failures We now are able for RESTEasy Reactive to determine the URI template even when auth failures occur Fixes: #24938 --- .../deployment/ObservabilityProcessor.java | 32 ++++- .../observability/ObservabilityHandler.java | 8 +- .../ObservabilityIntegrationRecorder.java | 128 ++++++++++++++++++ .../observability/ObservabilityUtil.java | 15 ++ .../http/deployment/FilterBuildItem.java | 18 +++ 5 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java index 62214df4910828..54a8626b5fbead 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java @@ -13,19 +13,21 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityCustomizer; +import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityIntegrationRecorder; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.runtime.metrics.MetricsFactory; +import io.quarkus.vertx.http.deployment.FilterBuildItem; public class ObservabilityProcessor { @BuildStep - MethodScannerBuildItem integrateObservability(Capabilities capabilities, + MethodScannerBuildItem methodScanner(Capabilities capabilities, Optional metricsCapability) { - boolean integrationNeeded = (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || - (metricsCapability.isPresent() - && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); + boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability); if (!integrationNeeded) { return null; } @@ -38,4 +40,26 @@ public List scan(MethodInfo method, ClassInfo actualEndp }); } + @BuildStep + @Record(value = ExecutionTime.STATIC_INIT) + FilterBuildItem preAuthFailureFilter(Capabilities capabilities, + Optional metricsCapability, + ObservabilityIntegrationRecorder recorder, + ResteasyReactiveDeploymentBuildItem deployment) { + boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability); + if (!integrationNeeded) { + return null; + } + + return FilterBuildItem.ofPreAuthenticationFailureHandler( + recorder.preAuthFailureHandler(deployment.getDeployment())); + } + + private boolean integrationNeeded(Capabilities capabilities, + Optional metricsCapability) { + return capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || + (metricsCapability.isPresent() + && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)); + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java index 3e49617d012d8a..a448289a892493 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java @@ -1,11 +1,12 @@ package io.quarkus.resteasy.reactive.server.runtime.observability; +import static io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityUtil.*; + import java.util.regex.Pattern; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; -import io.vertx.core.http.impl.HttpServerRequestInternal; import io.vertx.ext.web.RoutingContext; public class ObservabilityHandler implements ServerRestHandler { @@ -25,8 +26,7 @@ public void setTemplatePath(String templatePath) { @Override public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { - - ((HttpServerRequestInternal) (requestContext.unwrap(RoutingContext.class).request())).context() - .putLocal("UrlPathTemplate", templatePath); + setUrlPathTemplate(requestContext.unwrap(RoutingContext.class), templatePath); } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java new file mode 100644 index 00000000000000..72acb0831b1e64 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java @@ -0,0 +1,128 @@ +package io.quarkus.resteasy.reactive.server.runtime.observability; + +import static io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityUtil.*; + +import jakarta.ws.rs.HttpMethod; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.core.Deployment; +import org.jboss.resteasy.reactive.server.handlers.ClassRoutingHandler; +import org.jboss.resteasy.reactive.server.mapping.RequestMapper; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.security.AuthenticationException; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.UnauthorizedException; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class ObservabilityIntegrationRecorder { + + private static final Logger log = Logger.getLogger(ObservabilityIntegrationRecorder.class); + + /** + * Returns a handler that sets the special property URI Template path needed by various observability integrations + */ + public Handler preAuthFailureHandler(RuntimeValue deploymentRV) { + return new Handler() { + @Override + public void handle(RoutingContext event) { + if (shouldHandle(event)) { + try { + setTemplatePath(event, deploymentRV.getValue()); + } catch (Exception e) { + log.debug("Unable to set template path for observability", e); + } + } + event.next(); + } + + private boolean shouldHandle(RoutingContext event) { + if (!event.failed()) { + return false; + } + return event.failure() instanceof AuthenticationException + || event.failure() instanceof ForbiddenException + || event.failure() instanceof UnauthorizedException; + } + + private void setTemplatePath(RoutingContext rc, Deployment deployment) { + // do what RestInitialHandler does + var initMappers = new RequestMapper<>(deployment.getClassMappers()); + var requestMatch = initMappers.map(getPathWithoutPrefix(rc, deployment)); + var remaining = requestMatch.remaining.isEmpty() ? "/" : requestMatch.remaining; + + var serverRestHandlers = requestMatch.value.handlers; + if (serverRestHandlers == null || serverRestHandlers.length < 1) { + // nothing we can do + return; + } + var firstHandler = serverRestHandlers[0]; + if (!(firstHandler instanceof ClassRoutingHandler)) { + // nothing we can do + return; + } + + var classRoutingHandler = (ClassRoutingHandler) firstHandler; + var mappers = classRoutingHandler.getMappers(); + + var requestMethod = rc.request().method().name(); + + // do what ClassRoutingHandler does + var mapper = mappers.get(requestMethod); + if (mapper == null) { + if (requestMethod.equals(HttpMethod.HEAD) || requestMethod.equals(HttpMethod.OPTIONS)) { + mapper = mappers.get(HttpMethod.GET); + } + if (mapper == null) { + mapper = mappers.get(null); + } + if (mapper == null) { + // can't match the path + return; + } + } + var target = mapper.map(remaining); + if (target == null) { + if (requestMethod.equals(HttpMethod.HEAD)) { + mapper = mappers.get(HttpMethod.GET); + if (mapper != null) { + target = mapper.map(remaining); + } + } + + if (target == null) { + // can't match the path + return; + } + } + + var templatePath = requestMatch.template.template + target.template.template; + if (templatePath.endsWith("/")) { + templatePath = templatePath.substring(0, templatePath.length() - 1); + } + + setUrlPathTemplate(rc, templatePath); + } + + public String getPath(RoutingContext rc) { + return rc.normalizedPath(); + } + + public String getPathWithoutPrefix(RoutingContext rc, Deployment deployment) { + String path = getPath(rc); + if (path != null) { + String prefix = deployment.getPrefix(); + if (!prefix.isEmpty()) { + if (path.startsWith(prefix)) { + return path.substring(prefix.length()); + } + } + } + return path; + } + }; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java new file mode 100644 index 00000000000000..2576e8a7de7d43 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java @@ -0,0 +1,15 @@ +package io.quarkus.resteasy.reactive.server.runtime.observability; + +import io.vertx.core.http.impl.HttpServerRequestInternal; +import io.vertx.ext.web.RoutingContext; + +final class ObservabilityUtil { + + private ObservabilityUtil() { + } + + static void setUrlPathTemplate(RoutingContext routingContext, String templatePath) { + ((HttpServerRequestInternal) (routingContext.request())).context() + .putLocal("UrlPathTemplate", templatePath); + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java index ab7c6f4fd9883c..b9c6c012347b2b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java @@ -35,6 +35,15 @@ public FilterBuildItem(Handler handler, int priority) { this.isFailureHandler = false; } + private FilterBuildItem(Handler handler, int priority, boolean checkPriority, boolean isFailureHandler) { + this.handler = handler; + if (checkPriority) { + checkPriority(priority); + } + this.priority = priority; + this.isFailureHandler = isFailureHandler; + } + /** * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. * @@ -54,6 +63,15 @@ public static FilterBuildItem ofAuthenticationFailureHandler(Handler authFailureHandler) { + return new FilterBuildItem(authFailureHandler, AUTH_FAILURE_HANDLER + 1, false, true); + } + private void checkPriority(int priority) { if (priority < 0) { throw new IllegalArgumentException("`priority` must be positive");