diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityIntegrationBuildItem.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityIntegrationBuildItem.java new file mode 100644 index 0000000000000..6db8506c6f418 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityIntegrationBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkus.resteasy.reactive.server.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * A marker build item signifying that observability features have been integrated + */ +public final class ObservabilityIntegrationBuildItem extends SimpleBuildItem { +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java index 54a8626b5fbea..c6625226dcd1b 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java @@ -12,6 +12,7 @@ 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.ExecutionTime; import io.quarkus.deployment.annotations.Record; @@ -42,17 +43,20 @@ public List scan(MethodInfo method, ClassInfo actualEndp @BuildStep @Record(value = ExecutionTime.STATIC_INIT) - FilterBuildItem preAuthFailureFilter(Capabilities capabilities, + void preAuthFailureFilter(Capabilities capabilities, Optional metricsCapability, ObservabilityIntegrationRecorder recorder, - ResteasyReactiveDeploymentBuildItem deployment) { + ResteasyReactiveDeploymentBuildItem deployment, + BuildProducer filterProducer, + BuildProducer observabilityIntegrationProducer) { boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability); if (!integrationNeeded) { - return null; + return; } - return FilterBuildItem.ofPreAuthenticationFailureHandler( - recorder.preAuthFailureHandler(deployment.getDeployment())); + filterProducer.produce(FilterBuildItem.ofPreAuthenticationFailureHandler( + recorder.preAuthFailureHandler(deployment.getDeployment()))); + observabilityIntegrationProducer.produce(new ObservabilityIntegrationBuildItem()); } private boolean integrationNeeded(Capabilities capabilities, diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 8fbaa8e3f20db..a6e846be18697 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -1365,9 +1365,13 @@ private static boolean notFoundCustomExMapper(String builtInExSignature, String @BuildStep @Record(value = ExecutionTime.STATIC_INIT) - public FilterBuildItem addDefaultAuthFailureHandler(ResteasyReactiveRecorder recorder) { + public FilterBuildItem addDefaultAuthFailureHandler(ResteasyReactiveRecorder recorder, + ResteasyReactiveDeploymentBuildItem deployment, + Optional observabilityIntegrationBuildItem) { // replace default auth failure handler added by vertx-http so that our exception mappers can customize response - return new FilterBuildItem(recorder.defaultAuthFailureHandler(), FilterBuildItem.AUTHENTICATION - 1); + return new FilterBuildItem( + recorder.defaultAuthFailureHandler(deployment.getDeployment(), observabilityIntegrationBuildItem.isPresent()), + FilterBuildItem.AUTHENTICATION - 1); } private void checkForDuplicateEndpoint(ResteasyReactiveConfig config, Map> allMethods) { diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index e54d35e3229ec..7d0a6f1231ae1 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -53,6 +53,7 @@ import io.quarkus.resteasy.reactive.common.runtime.ArcBeanFactory; import io.quarkus.resteasy.reactive.common.runtime.ArcThreadSetupAction; import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveCommonRecorder; +import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityIntegrationRecorder; import io.quarkus.runtime.BlockingOperationControl; import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.runtime.LaunchMode; @@ -341,11 +342,16 @@ public ServerSerialisers createServerSerialisers() { return new ServerSerialisers(); } - public Handler defaultAuthFailureHandler() { + public Handler defaultAuthFailureHandler( + RuntimeValue deployment, boolean setTemplatePath) { return new Handler() { @Override public void handle(RoutingContext event) { if (event.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof DefaultAuthFailureHandler) { + if (setTemplatePath) { + ObservabilityIntegrationRecorder.setTemplatePath(event, deployment.getValue()); + } + // fail event rather than end it, so it's handled by abort handlers (see #addFailureHandler method) event.put(QuarkusHttpUser.AUTH_FAILURE_HANDLER, new FailingDefaultAuthFailureHandler()); } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java index 72acb0831b1e6..3e550fa61c9e4 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java @@ -47,82 +47,85 @@ private boolean shouldHandle(RoutingContext event) { || 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; - } + public static void setTemplatePath(RoutingContext rc, Deployment deployment) { + // do what RestInitialHandler does + var initMappers = new RequestMapper<>(deployment.getClassMappers()); + var requestMatch = initMappers.map(getPathWithoutPrefix(rc, deployment)); + if (requestMatch == null) { + return; + } + 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); } - 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; - } - } + 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); - } + var templatePath = requestMatch.template.template + target.template.template; + if (templatePath.endsWith("/")) { + templatePath = templatePath.substring(0, templatePath.length() - 1); + } - setUrlPathTemplate(rc, templatePath); - } + setUrlPathTemplate(rc, templatePath); + } - public String getPath(RoutingContext rc) { - return rc.normalizedPath(); - } + private static 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()); - } - } + private static 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; } - }; + } + return path; } } diff --git a/integration-tests/micrometer-security/pom.xml b/integration-tests/micrometer-security/pom.xml new file mode 100644 index 0000000000000..9422bf687a856 --- /dev/null +++ b/integration-tests/micrometer-security/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + + quarkus-integration-test-micrometer-security + Quarkus - Integration Tests - Micrometer Security + + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + + + io.quarkus + quarkus-rest-jackson + + + + + io.quarkus + quarkus-security + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-micrometer-registry-prometheus-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-security-deployment + ${project.version} + pom + test + + + * + * + + + + + org.awaitility + awaitility + test + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + diff --git a/integration-tests/micrometer-security/src/main/java/io/quarkus/it/micrometer/security/SecuredResource.java b/integration-tests/micrometer-security/src/main/java/io/quarkus/it/micrometer/security/SecuredResource.java new file mode 100644 index 0000000000000..2b135170ac259 --- /dev/null +++ b/integration-tests/micrometer-security/src/main/java/io/quarkus/it/micrometer/security/SecuredResource.java @@ -0,0 +1,20 @@ +package io.quarkus.it.micrometer.security; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.smallrye.mutiny.Uni; + +@Path("/secured") +public class SecuredResource { + + @GET + @Path("/{message}") + @Produces(MediaType.TEXT_PLAIN) + public Uni message(@PathParam("message") String message) { + return Uni.createFrom().item(message); + } +} diff --git a/integration-tests/micrometer-security/src/main/resources/application.properties b/integration-tests/micrometer-security/src/main/resources/application.properties new file mode 100644 index 0000000000000..74f64a800e5bf --- /dev/null +++ b/integration-tests/micrometer-security/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.http.auth.permission.default.paths=/secured/* +quarkus.http.auth.permission.default.policy=authenticated diff --git a/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java b/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java new file mode 100644 index 0000000000000..dfed5aab9f601 --- /dev/null +++ b/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java @@ -0,0 +1,32 @@ +package io.quarkus.it.micrometer.security; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.not; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class SecuredResourceTest { + + @Test + void testMetricsForUnauthorizedRequest() { + when().get("/secured/foo") + .then() + .statusCode(403); + + when().get("/q/metrics") + .then() + .statusCode(200) + .body( + allOf( + not(containsString("/secured/foo")), + containsString("/secured/{message}")) + + ); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 355b11472b6ae..0a63ea27ed39a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -347,6 +347,7 @@ elasticsearch-java-client micrometer-mp-metrics micrometer-prometheus + micrometer-security opentelemetry opentelemetry-quickstart opentelemetry-spi