Skip to content

Commit

Permalink
Merge pull request #34702 from geoand/#24938
Browse files Browse the repository at this point in the history
Properly populate metrics uri in presence of auth failures
  • Loading branch information
geoand authored Jul 13, 2023
2 parents 9e97dc8 + 51385e6 commit fb45252
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetricsCapabilityBuildItem> metricsCapability) {
boolean integrationNeeded = (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) ||
(metricsCapability.isPresent()
&& metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)));
boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability);
if (!integrationNeeded) {
return null;
}
Expand All @@ -38,4 +40,26 @@ public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndp
});
}

@BuildStep
@Record(value = ExecutionTime.STATIC_INIT)
FilterBuildItem preAuthFailureFilter(Capabilities capabilities,
Optional<MetricsCapabilityBuildItem> 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<MetricsCapabilityBuildItem> metricsCapability) {
return capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) ||
(metricsCapability.isPresent()
&& metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER));
}

}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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<RoutingContext> preAuthFailureHandler(RuntimeValue<Deployment> deploymentRV) {
return new Handler<RoutingContext>() {
@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;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public FilterBuildItem(Handler<RoutingContext> handler, int priority) {
this.isFailureHandler = false;
}

private FilterBuildItem(Handler<RoutingContext> 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.
*
Expand All @@ -54,6 +63,15 @@ public static FilterBuildItem ofAuthenticationFailureHandler(Handler<RoutingCont
return new FilterBuildItem(authFailureHandler);
}

/**
* Creates a new instance of {@link FilterBuildItem} with an authentication failure handler.
* The handler will be added right before any handlers added by
* {@link FilterBuildItem#ofAuthenticationFailureHandler(Handler)}
*/
public static FilterBuildItem ofPreAuthenticationFailureHandler(Handler<RoutingContext> 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");
Expand Down
19 changes: 19 additions & 0 deletions integration-tests/micrometer-prometheus/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>

<!-- Security -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down Expand Up @@ -163,6 +169,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.it.micrometer.prometheus;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@Path("/secured")
public class SecuredResource {

@GET
@Path("item/{id}")
public String item(@PathParam("id") String id) {
return "return message with id " + id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ deployment.env=test

# Disable Kubernetes dev services as not supported on Windows
quarkus.kubernetes-client.devservices.enabled=false


quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.scott=reader
quarkus.security.users.embedded.users.stuart=writer
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/*

Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ void testTemplatedPathOnClass() {

@Test
@Order(10)
void testSecuredEndpoint() {
when().get("/secured/item/123").then().statusCode(401);
given().auth().preemptive().basic("foo", "bar").when().get("/secured/item/321").then().statusCode(401);
given().auth().preemptive().basic("scott", "reader").when().get("/secured/item/123").then().statusCode(200);
given().auth().preemptive().basic("stuart", "writer").when().get("/secured/item/321").then().statusCode(200);
}

@Test
@Order(11)
void testPrometheusScrapeEndpointTextPlain() {
RestAssured.given().header("Accept", TextFormat.CONTENT_TYPE_004)
.when().get("/q/metrics")
Expand All @@ -111,6 +120,10 @@ void testPrometheusScrapeEndpointTextPlain() {
.body(containsString("status=\"200\""))
.body(containsString("uri=\"/message\""))
.body(containsString("uri=\"/message/item/{id}\""))
.body(containsString("status=\"200\",uri=\"/message/item/{id}\""))
.body(containsString("uri=\"/secured/item/{id}\""))
.body(containsString("status=\"200\",uri=\"/secured/item/{id}\""))
.body(containsString("status=\"401\",uri=\"/secured/item/{id}\""))
.body(containsString("outcome=\"SUCCESS\""))
.body(containsString("dummy=\"value\""))
.body(containsString("foo=\"bar\""))
Expand Down
19 changes: 19 additions & 0 deletions integration-tests/opentelemetry-reactive/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
<artifactId>opentelemetry-sdk-testing</artifactId>
</dependency>

<!-- Security -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down Expand Up @@ -121,6 +127,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.it.opentelemetry.reactive;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.RestPath;

@Path("secured")
public class SecuredResource {
@GET
@Path("item/{value}")
public String get(@RestPath String value) {
return "Received: " + value;
}
}
Loading

0 comments on commit fb45252

Please sign in to comment.