From c8c8321e021a2f7fb734b5d6ba88c17d247915d5 Mon Sep 17 00:00:00 2001 From: brunobat Date: Mon, 21 Oct 2024 10:57:20 +0100 Subject: [PATCH 01/38] Micrometer exemplars on HTTP (cherry picked from commit fffb6cb5f90afaf20b9f80d89244004a8b25eb4a) --- .../deployment/MicrometerProcessor.java | 10 ++++++ .../export/PrometheusRegistryProcessor.java | 8 +++-- .../binder/vertx/VertxHttpServerMetrics.java | 20 ++++++++--- .../binder/vertx/VertxMeterBinderAdapter.java | 11 ++++-- .../vertx/VertxMeterBinderRecorder.java | 7 ++-- .../EmptyExemplarSamplerProvider.java | 2 +- ...OpenTelemetryExemplarContextUnwrapper.java | 16 +++++++++ .../OpenTelemetryContextUnwrapper.java | 18 ++++++++++ ...OpenTelemetryExemplarContextUnwrapper.java | 31 ++++++++++++++++ .../OpentelemetryExemplarSamplerProvider.java | 2 +- .../prometheus/ExemplarOffTest.java | 34 ++++++++++++++++++ .../micrometer/prometheus/ExemplarTest.java | 35 +++++++++++++++++++ .../micrometer/prometheus/OtelOffProfile.java | 16 +++++++++ .../micrometer/prometheus/OtelOnProfile.java | 16 +++++++++ 14 files changed, 213 insertions(+), 13 deletions(-) rename extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/{ => exemplars}/EmptyExemplarSamplerProvider.java (83%) create mode 100644 extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/NoopOpenTelemetryExemplarContextUnwrapper.java create mode 100644 extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryContextUnwrapper.java create mode 100644 extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryExemplarContextUnwrapper.java rename extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/{ => exemplars}/OpentelemetryExemplarSamplerProvider.java (96%) create mode 100644 integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarOffTest.java create mode 100644 integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarTest.java create mode 100644 integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOffProfile.java create mode 100644 integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOnProfile.java diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java index 8ae781a2d7e43..0fa56ac6a302f 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java @@ -58,6 +58,7 @@ import io.quarkus.micrometer.runtime.MicrometerRecorder; import io.quarkus.micrometer.runtime.MicrometerTimedInterceptor; import io.quarkus.micrometer.runtime.config.MicrometerConfig; +import io.quarkus.micrometer.runtime.export.exemplars.NoopOpenTelemetryExemplarContextUnwrapper; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.metrics.MetricsFactory; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; @@ -93,6 +94,15 @@ MetricsCapabilityBuildItem metricsCapabilityBuildItem() { null); } + @BuildStep(onlyIfNot = PrometheusRegistryProcessor.PrometheusEnabled.class) + void registerEmptyExamplarProvider( + BuildProducer additionalBeans) { + additionalBeans.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(NoopOpenTelemetryExemplarContextUnwrapper.class) + .setUnremovable() + .build()); + } + @BuildStep(onlyIf = { PrometheusRegistryProcessor.PrometheusEnabled.class }) MetricsCapabilityBuildItem metricsCapabilityPrometheusBuildItem( NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java index 10ed43b3dca42..405ebd2be7dca 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java @@ -16,9 +16,11 @@ import io.quarkus.micrometer.runtime.MicrometerRecorder; import io.quarkus.micrometer.runtime.config.MicrometerConfig; import io.quarkus.micrometer.runtime.config.PrometheusConfigGroup; -import io.quarkus.micrometer.runtime.export.EmptyExemplarSamplerProvider; -import io.quarkus.micrometer.runtime.export.OpentelemetryExemplarSamplerProvider; import io.quarkus.micrometer.runtime.export.PrometheusRecorder; +import io.quarkus.micrometer.runtime.export.exemplars.EmptyExemplarSamplerProvider; +import io.quarkus.micrometer.runtime.export.exemplars.NoopOpenTelemetryExemplarContextUnwrapper; +import io.quarkus.micrometer.runtime.export.exemplars.OpenTelemetryExemplarContextUnwrapper; +import io.quarkus.micrometer.runtime.export.exemplars.OpentelemetryExemplarSamplerProvider; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; @@ -73,6 +75,7 @@ void registerOpentelemetryExemplarSamplerProvider( BuildProducer additionalBeans) { additionalBeans.produce(AdditionalBeanBuildItem.builder() .addBeanClass(OpentelemetryExemplarSamplerProvider.class) + .addBeanClass(OpenTelemetryExemplarContextUnwrapper.class) .setUnremovable() .build()); } @@ -82,6 +85,7 @@ void registerEmptyExamplarProvider( BuildProducer additionalBeans) { additionalBeans.produce(AdditionalBeanBuildItem.builder() .addBeanClass(EmptyExemplarSamplerProvider.class) + .addBeanClass(NoopOpenTelemetryExemplarContextUnwrapper.class) .setUnremovable() .build()); } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java index a7993195d294a..9b9cebe55007b 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java @@ -20,6 +20,7 @@ import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor; import io.quarkus.micrometer.runtime.binder.HttpBinderConfiguration; import io.quarkus.micrometer.runtime.binder.HttpCommonTags; +import io.quarkus.micrometer.runtime.export.exemplars.OpenTelemetryContextUnwrapper; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.ServerWebSocket; @@ -40,6 +41,7 @@ public class VertxHttpServerMetrics extends VertxTcpServerMetrics static final Logger log = Logger.getLogger(VertxHttpServerMetrics.class); HttpBinderConfiguration config; + OpenTelemetryContextUnwrapper openTelemetryContextUnwrapper; final LongAdder activeRequests; @@ -49,9 +51,12 @@ public class VertxHttpServerMetrics extends VertxTcpServerMetrics private final List httpServerMetricsTagsContributors; - VertxHttpServerMetrics(MeterRegistry registry, HttpBinderConfiguration config) { + VertxHttpServerMetrics(MeterRegistry registry, + HttpBinderConfiguration config, + OpenTelemetryContextUnwrapper openTelemetryContextUnwrapper) { super(registry, "http.server", null); this.config = config; + this.openTelemetryContextUnwrapper = openTelemetryContextUnwrapper; activeRequests = new LongAdder(); Gauge.builder(config.getHttpServerActiveRequestsName(), activeRequests, LongAdder::doubleValue) @@ -164,12 +169,14 @@ public void requestReset(HttpRequestMetric requestMetric) { if (path != null) { Timer.Sample sample = requestMetric.getSample(); - sample.stop(requestsTimer - .withTags(Tags.of( + openTelemetryContextUnwrapper.executeInContext( + sample::stop, + requestsTimer.withTags(Tags.of( VertxMetricsTags.method(requestMetric.request().method()), HttpCommonTags.uri(path, requestMetric.initialPath, 0), Outcome.CLIENT_ERROR.asTag(), - HttpCommonTags.STATUS_RESET))); + HttpCommonTags.STATUS_RESET)), + requestMetric.request().context()); } requestMetric.requestEnded(); } @@ -207,7 +214,10 @@ public void responseEnd(HttpRequestMetric requestMetric, HttpResponse response, } } - sample.stop(requestsTimer.withTags(allTags)); + openTelemetryContextUnwrapper.executeInContext( + sample::stop, + requestsTimer.withTags(allTags), + requestMetric.request().context()); } requestMetric.requestEnded(); } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java index 473f4bf0a1be5..6b9b7625de565 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java @@ -11,6 +11,7 @@ import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.quarkus.micrometer.runtime.binder.HttpBinderConfiguration; +import io.quarkus.micrometer.runtime.export.exemplars.OpenTelemetryContextUnwrapper; import io.quarkus.vertx.http.runtime.ExtendedQuarkusVertxHttpMetrics; import io.vertx.core.VertxOptions; import io.vertx.core.datagram.DatagramSocketOptions; @@ -37,11 +38,14 @@ public class VertxMeterBinderAdapter extends MetricsOptions public static final String METRIC_NAME_SEPARATOR = "|"; private HttpBinderConfiguration httpBinderConfiguration; + private OpenTelemetryContextUnwrapper openTelemetryContextUnwrapper; public VertxMeterBinderAdapter() { } - void setHttpConfig(HttpBinderConfiguration httpBinderConfiguration) { + void initBinder(HttpBinderConfiguration httpBinderConfiguration, + OpenTelemetryContextUnwrapper openTelemetryContextUnwrapper) { + this.openTelemetryContextUnwrapper = openTelemetryContextUnwrapper; this.httpBinderConfiguration = httpBinderConfiguration; } @@ -70,9 +74,12 @@ public MetricsOptions newOptions() { if (httpBinderConfiguration == null) { throw new NoStackTraceException("HttpBinderConfiguration was not found"); } + if (openTelemetryContextUnwrapper == null) { + throw new NoStackTraceException("OpenTelemetryContextUnwrapper was not found"); + } if (httpBinderConfiguration.isServerEnabled()) { log.debugf("Create HttpServerMetrics with options %s and address %s", options, localAddress); - return new VertxHttpServerMetrics(Metrics.globalRegistry, httpBinderConfiguration); + return new VertxHttpServerMetrics(Metrics.globalRegistry, httpBinderConfiguration, openTelemetryContextUnwrapper); } return null; } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRecorder.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRecorder.java index 37b02d95ae654..151fae9c61d23 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRecorder.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRecorder.java @@ -6,6 +6,7 @@ import io.quarkus.arc.Arc; import io.quarkus.micrometer.runtime.binder.HttpBinderConfiguration; +import io.quarkus.micrometer.runtime.export.exemplars.OpenTelemetryContextUnwrapper; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.VertxOptions; @@ -30,18 +31,20 @@ public void accept(VertxOptions vertxOptions) { /* RUNTIME_INIT */ public void configureBinderAdapter() { HttpBinderConfiguration httpConfig = Arc.container().instance(HttpBinderConfiguration.class).get(); + OpenTelemetryContextUnwrapper openTelemetryContextUnwrapper = Arc.container() + .instance(OpenTelemetryContextUnwrapper.class).get(); if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { if (devModeConfig == null) { // Create an object whose attributes we can update devModeConfig = httpConfig.unwrap(); - binderAdapter.setHttpConfig(devModeConfig); + binderAdapter.initBinder(devModeConfig, openTelemetryContextUnwrapper); } else { // update config attributes devModeConfig.update(httpConfig); } } else { // unwrap the CDI bean (use POJO) - binderAdapter.setHttpConfig(httpConfig.unwrap()); + binderAdapter.initBinder(httpConfig.unwrap(), openTelemetryContextUnwrapper); } } } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/EmptyExemplarSamplerProvider.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/EmptyExemplarSamplerProvider.java similarity index 83% rename from extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/EmptyExemplarSamplerProvider.java rename to extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/EmptyExemplarSamplerProvider.java index 46dc7a94978d4..876f2cb7ffce9 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/EmptyExemplarSamplerProvider.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/EmptyExemplarSamplerProvider.java @@ -1,4 +1,4 @@ -package io.quarkus.micrometer.runtime.export; +package io.quarkus.micrometer.runtime.export.exemplars; import java.util.Optional; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/NoopOpenTelemetryExemplarContextUnwrapper.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/NoopOpenTelemetryExemplarContextUnwrapper.java new file mode 100644 index 0000000000000..dd9980efc666c --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/NoopOpenTelemetryExemplarContextUnwrapper.java @@ -0,0 +1,16 @@ +package io.quarkus.micrometer.runtime.export.exemplars; + +import java.util.function.Function; + +import jakarta.enterprise.context.Dependent; + +import io.vertx.core.Context; + +@Dependent +public class NoopOpenTelemetryExemplarContextUnwrapper implements OpenTelemetryContextUnwrapper { + + @Override + public R executeInContext(Function methodReference, P parameter, Context requestContext) { + return methodReference.apply(parameter);// pass through + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryContextUnwrapper.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryContextUnwrapper.java new file mode 100644 index 0000000000000..f01fee0723052 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryContextUnwrapper.java @@ -0,0 +1,18 @@ +package io.quarkus.micrometer.runtime.export.exemplars; + +import java.util.function.Function; + +public interface OpenTelemetryContextUnwrapper { + /** + * Called when an HTTP server response has ended. + * Makes sure exemplars are produced because they have an OTel context. + * + * @param methodReference Ex: Sample stop method reference + * @param parameter The parameter to pass to the method + * @param requestContext The request context + * @param

The parameter type is a type of metric, ex: Timer + * @param The return type of the method pointed by the methodReference + * @return The result of the method + */ + R executeInContext(Function methodReference, P parameter, io.vertx.core.Context requestContext); +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryExemplarContextUnwrapper.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryExemplarContextUnwrapper.java new file mode 100644 index 0000000000000..68ac7d6765a5d --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpenTelemetryExemplarContextUnwrapper.java @@ -0,0 +1,31 @@ +package io.quarkus.micrometer.runtime.export.exemplars; + +import java.util.function.Function; + +import jakarta.enterprise.context.Dependent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; + +@Dependent +public class OpenTelemetryExemplarContextUnwrapper implements OpenTelemetryContextUnwrapper { + + @Override + public R executeInContext(Function methodReference, P parameter, io.vertx.core.Context requestContext) { + if (requestContext == null) { + return methodReference.apply(parameter); + } + + Context newContext = QuarkusContextStorage.getContext(requestContext); + + if (newContext == null) { + return methodReference.apply(parameter); + } + + io.opentelemetry.context.Context oldContext = QuarkusContextStorage.INSTANCE.current(); + try (Scope scope = QuarkusContextStorage.INSTANCE.attach(newContext)) { + return methodReference.apply(parameter); + } + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/OpentelemetryExemplarSamplerProvider.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpentelemetryExemplarSamplerProvider.java similarity index 96% rename from extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/OpentelemetryExemplarSamplerProvider.java rename to extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpentelemetryExemplarSamplerProvider.java index d99131fb7831b..4a6e87a29d9f7 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/OpentelemetryExemplarSamplerProvider.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/exemplars/OpentelemetryExemplarSamplerProvider.java @@ -1,4 +1,4 @@ -package io.quarkus.micrometer.runtime.export; +package io.quarkus.micrometer.runtime.export.exemplars; import java.util.Optional; import java.util.function.Function; diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarOffTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarOffTest.java new file mode 100644 index 0000000000000..d46a50539b618 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarOffTest.java @@ -0,0 +1,34 @@ +package io.quarkus.it.micrometer.prometheus; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.when; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +/** + * See Micrometer Guide + */ +@QuarkusTest +@TestProfile(OtelOffProfile.class) +public class ExemplarOffTest { + + @Test + void testExemplar() { + when().get("/example/prime/257").then().statusCode(200); + when().get("/example/prime/7919").then().statusCode(200); + + String metricMatch = "http_server_requests_seconds_count{dummy=\"value\",env=\"test\"," + + "env2=\"test\",foo=\"UNSET\",method=\"GET\",outcome=\"SUCCESS\"," + + "registry=\"prometheus\",status=\"200\",uri=\"/example/prime/{number}\"} 2.0 # {span_id=\""; + + await().atMost(5, SECONDS).untilAsserted(() -> { + assertFalse(get("/q/metrics").then().extract().asString().contains(metricMatch)); + }); + } +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarTest.java new file mode 100644 index 0000000000000..50e94d6220ae0 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExemplarTest.java @@ -0,0 +1,35 @@ +package io.quarkus.it.micrometer.prometheus; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.when; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +/** + * See Micrometer Guide + */ +@QuarkusTest +@TestProfile(OtelOnProfile.class) +public class ExemplarTest { + + @Test + void testExemplar() { + when().get("/example/prime/257").then().statusCode(200); + when().get("/example/prime/7919").then().statusCode(200); + + String metricMatch = "http_server_requests_seconds_count{dummy=\"value\",env=\"test\"," + + "env2=\"test\",foo=\"UNSET\",method=\"GET\",outcome=\"SUCCESS\"," + + "registry=\"prometheus\",status=\"200\",uri=\"/example/prime/{number}\"} 2.0 # {span_id=\""; + + await().atMost(5, SECONDS).untilAsserted(() -> { + String body = get("/q/metrics").then().extract().asString(); + assertTrue(body.contains(metricMatch), body); + }); + } +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOffProfile.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOffProfile.java new file mode 100644 index 0000000000000..d0b00c7a6e76d --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOffProfile.java @@ -0,0 +1,16 @@ +package io.quarkus.it.micrometer.prometheus; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class OtelOffProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + Map config = new HashMap<>(Map.of( + "quarkus.otel.enabled", "false")); + return config; + } +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOnProfile.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOnProfile.java new file mode 100644 index 0000000000000..7d6e9084af52b --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/OtelOnProfile.java @@ -0,0 +1,16 @@ +package io.quarkus.it.micrometer.prometheus; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class OtelOnProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + Map config = new HashMap<>(Map.of( + "quarkus.otel.enabled", "true")); + return config; + } +} From fa900c3e2ed61847cf95d6413876ba71e926a26c Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 20 Nov 2024 10:51:06 +0100 Subject: [PATCH 02/38] Docs: dev mode differences - remove the link for old Dev UI - old Dev UI was removed in 3.4.0.CR1 (cherry picked from commit 3eae5b7c20696ee698bb4b6c3c879ada5f8cb4e6) --- docs/src/main/asciidoc/dev-mode-differences.adoc | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/src/main/asciidoc/dev-mode-differences.adoc b/docs/src/main/asciidoc/dev-mode-differences.adoc index ed9a6ecd4656b..d7d2f87ad9a6b 100644 --- a/docs/src/main/asciidoc/dev-mode-differences.adoc +++ b/docs/src/main/asciidoc/dev-mode-differences.adoc @@ -46,13 +46,6 @@ Examples of such operations are: * Running scheduled operations * Building a container -[TIP] -==== -A new Dev UI has been implemented in Quarkus 3.x. -Not all the features are available yet. -You can still access the previous version of the Dev UI using: http://localhost:8080/q/dev-ui/. -==== - === Error pages In an effort to make development errors very easy to diagnose, Quarkus provides various detailed error pages when running in dev mode. From a9d90c5335e7a12435c025682579e420d973871b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:52:20 +0000 Subject: [PATCH 03/38] Bump smallrye-fault-tolerance.version from 6.6.2 to 6.6.3 Bumps `smallrye-fault-tolerance.version` from 6.6.2 to 6.6.3. Updates `io.smallrye:smallrye-fault-tolerance` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-api` from 6.6.2 to 6.6.3 - [Release notes](https://github.com/smallrye/smallrye-fault-tolerance/releases) - [Commits](https://github.com/smallrye/smallrye-fault-tolerance/compare/6.6.2...6.6.3) Updates `io.smallrye:smallrye-fault-tolerance-autoconfig-core` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-core` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-context-propagation` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-kotlin` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-mutiny` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-tracing-propagation` from 6.6.2 to 6.6.3 Updates `io.smallrye:smallrye-fault-tolerance-vertx` from 6.6.2 to 6.6.3 --- updated-dependencies: - dependency-name: io.smallrye:smallrye-fault-tolerance dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-api dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-autoconfig-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-context-propagation dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-kotlin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-mutiny dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-tracing-propagation dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.smallrye:smallrye-fault-tolerance-vertx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 803581afd33c8632777560dd9ba00f19f1c05bd1) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f23973fc842db..06a640bf68baa 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -53,7 +53,7 @@ 4.0.0 4.0.3 2.11.0 - 6.6.2 + 6.6.3 4.6.1 2.1.2 1.0.13 From c05e8e92ce35fa9d5e861e2a89494bd85167420e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Wed, 20 Nov 2024 12:45:44 +0100 Subject: [PATCH 04/38] Google Cloud Function gen 2 is now the default (cherry picked from commit 194a96619bfc5bfea92c73005994b08658c256a4) --- docs/src/main/asciidoc/funqy-gcp-functions.adoc | 10 +--------- docs/src/main/asciidoc/gcp-functions-http.adoc | 4 +--- docs/src/main/asciidoc/gcp-functions.adoc | 12 +----------- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/docs/src/main/asciidoc/funqy-gcp-functions.adoc b/docs/src/main/asciidoc/funqy-gcp-functions.adoc index 85982963ee058..43b340ef6be8a 100644 --- a/docs/src/main/asciidoc/funqy-gcp-functions.adoc +++ b/docs/src/main/asciidoc/funqy-gcp-functions.adoc @@ -81,8 +81,6 @@ In this example, we will create two background functions and a cloud events func Background functions allow you to react to Google Cloud events like PubSub messages, Cloud Storage events, Firestore events, ... Cloud events functions allow you to react to supported events using the Cloud Events specification. -NOTE: Quarkus supports Cloud Functions gen 1 and gen 2. For an overview of Cloud Functions gen 2 see https://cloud.google.com/functions/docs/2nd-gen/overview[this page] on the Google Cloud Functions documentation. To use gen 2 you must add the `--gen2` parameter. - [source,java] ---- import jakarta.inject.Inject; @@ -231,8 +229,6 @@ You can also simply add a file to Cloud Storage using the command line of the we === Cloud Events Functions - Cloud Storage -WARNING: Cloud Events Function is a feature of Cloud Functions gen 2 only. - Before deploying your function, you need to create a bucket. [source,bash] @@ -244,7 +240,7 @@ Then, use this command to deploy to Google Cloud Functions: [source,bash] ---- -gcloud functions deploy quarkus-example-cloud-event --gen2 \ +gcloud functions deploy quarkus-example-cloud-event \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyCloudEventsFunction \ --runtime=java21 --trigger-bucket=example-cloud-event --source=target/deployment ---- @@ -324,8 +320,6 @@ This will call your PubSub background function with a Cloud Storage event `{"nam === Cloud Events Functions - Cloud Storage -WARNING: Cloud Events Function is a feature of Cloud Functions gen 2 only. - For cloud events functions, you launch the invoker with a target class of `io.quarkus.funqy.gcp.functions.FunqyCloudEventsFunction``. [source,bash,subs="attributes"] @@ -457,8 +451,6 @@ class GreetingFunctionsStorageTest { === Cloud Events Functions - Cloud Storage -WARNING: Cloud Events Function is a feature of Cloud Functions gen 2 only. - [source,java] ---- import static io.restassured.RestAssured.given; diff --git a/docs/src/main/asciidoc/gcp-functions-http.adoc b/docs/src/main/asciidoc/gcp-functions-http.adoc index 0d0040952902c..321b389abc284 100644 --- a/docs/src/main/asciidoc/gcp-functions-http.adoc +++ b/docs/src/main/asciidoc/gcp-functions-http.adoc @@ -63,13 +63,11 @@ one for Reactive routes and one for xref:funqy-http.adoc[Funqy HTTP]. [NOTE] ==== These various endpoints are for demonstration purposes. -For real life applications, you should choose one of this technology and stick to it. +For real life applications, you should choose one of these technologies and stick to it. ==== If you don't need endpoints of each type, you can remove the corresponding extensions from your `pom.xml`. -NOTE: Quarkus supports Cloud Functions gen 1 and gen 2. For an overview of Cloud Functions gen 2 see https://cloud.google.com/functions/docs/2nd-gen/overview[this page] on the Google Cloud Functions documentation. To use gen 2 you must and add the `--gen2` parameter. - === The Jakarta REST endpoint [source,java] diff --git a/docs/src/main/asciidoc/gcp-functions.adoc b/docs/src/main/asciidoc/gcp-functions.adoc index 4c3a53bb2ddc0..d6d0189ec493c 100644 --- a/docs/src/main/asciidoc/gcp-functions.adoc +++ b/docs/src/main/asciidoc/gcp-functions.adoc @@ -58,8 +58,6 @@ gcloud auth login For this example project, we will create four functions, one `HttpFunction`, one `BackgroundFunction` (Storage event), one `RawBackgroundFunction` (PubSub event) and one `CloudEventsFunction` (storage event using the Cloud Events specification). -NOTE: Quarkus supports Cloud Functions gen 1 and gen 2. For an overview of Cloud Functions gen 2 see https://cloud.google.com/functions/docs/2nd-gen/overview[this page] on the Google Cloud Functions documentation. To use gen 2 you must add the `--gen2` parameter. - == Choose Your Function The `quarkus-google-cloud-functions` extension scans your project for a class that directly implements the Google Cloud `HttpFunction`, `BackgroundFunction`, `RawBackgroundFunction` or `CloudEventsFunction` interface. @@ -193,8 +191,6 @@ public class RawBackgroundFunctionPubSubTest implements RawBackgroundFunction { === The CloudEventsFunction -WARNING: `CloudEventsFunction` is a feature of Cloud Functions gen 2 only. - This `CloudEventsFunction` is triggered by a Cloud Events Storage event, you can use any Cloud Events supported by Google Cloud instead. [source,java] @@ -332,14 +328,12 @@ gcloud functions call quarkus-example-pubsub --data '{"data":{"greeting":"world" === The CloudEventsFunction -WARNING: `CloudEventsFunction` is a feature of Cloud Functions gen 2 only. - This is an example command to deploy your `CloudEventsFunction` to Google Cloud, as the function is triggered by a Storage event, it needs to use `--trigger-bucket` parameter with the name of a previously created bucket: [source,bash] ---- -gcloud functions deploy quarkus-example-cloud-event --gen2 \ +gcloud functions deploy quarkus-example-cloud-event \ --entry-point=io.quarkus.gcp.functions.QuarkusCloudEventsFunction \ --runtime=java21 --trigger-bucket=example-cloud-event --source=target/deployment ---- @@ -429,8 +423,6 @@ This will call your PubSub background function with a PubSubMessage `{"greeting" === The CloudEventsFunction -IMPORTANT: `CloudEventsFunction` is a feature of Cloud Function gen 2 only. - For cloud events functions, you launch the invoker with a target class of `io.quarkus.gcp.functions.QuarkusCloudEventsFunction`. [source,bash,subs="attributes"] @@ -592,8 +584,6 @@ class RawBackgroundFunctionPubSubTestCase { === The CloudEventsFunction -WARNING: Cloud Events Function is a feature of Cloud Functions gen 2 only. - [source,java] ---- import static io.restassured.RestAssured.given; From aaa75cd7f48fcb95c50e598465aea585464ba34e Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Tue, 19 Nov 2024 22:23:14 +0100 Subject: [PATCH 05/38] Fix wrong generated web endpoint path on Windows The web endpoint path was created from file path which is differ on Windows. This caused enpoint path be like `/app\index-styles.js` (cherry picked from commit f9dbe64c78a4c61a659d0f24c71869268fef0972) --- .../locator/deployment/WebDependencyLocatorProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java index d0a840540fb2d..6ee11770182c9 100644 --- a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java @@ -85,6 +85,7 @@ public void findRelevantFiles(BuildProducer ge webstream.forEach(path -> { if (Files.isRegularFile(path)) { String endpoint = SLASH + web.relativize(path); + endpoint = endpoint.replace('\\', '/'); try { if (path.toString().endsWith(DOT_HTML)) { generatedStaticProducer.produce(new GeneratedStaticResourceBuildItem(endpoint, From 89fdf098289adc2f30aa8fb65396e5be8a5af9d6 Mon Sep 17 00:00:00 2001 From: Neon <1169307+neon-dev@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:49:23 +0100 Subject: [PATCH 06/38] Update qute.adoc (cherry picked from commit 3e41b3f39589b9c55df7370b7da099248e4321bf) --- docs/src/main/asciidoc/qute.adoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index f8a91fe6af198..30946c4bb4c92 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -145,8 +145,8 @@ Hello Martin! There's an alternate way to declare your templates in your Java code, which relies on the following convention: - Organize your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if - your `ItemResource` class references two templates `hello` and `goodbye`, place them at `/src/main/resources/templates/ItemResource/hello.txt` - and `/src/main/resources/templates/ItemResource/goodbye.txt`. Grouping templates per resource class makes it easier to navigate to them. + your `FruitResource` class references two templates `apples` and `oranges`, place them at `/src/main/resources/templates/FruitResource/apples.txt` + and `/src/main/resources/templates/FruitResource/oranges.txt`. Grouping templates per resource class makes it easier to navigate to them. - In each of your resource class, declare a `@CheckedTemplate static class Template {}` class within your resource class. - Declare one `public static native TemplateInstance method();` per template file for your resource. - Use those static methods to build your template instances. @@ -162,7 +162,7 @@ Hello {name}! <1> ---- <1> `{name}` is a value expression that is evaluated when the template is rendered. -Now let's declare and use those templates in the resource class. +Now let's declare and use this template in the resource class. .HelloResource.java [source,java] @@ -201,7 +201,7 @@ NOTE: Once you have declared a `@CheckedTemplate` class, we will check that all Keep in mind this style of declaration allows you to reference templates declared in other resources too: -.HelloResource.java +.GreetingResource.java [source,java] ---- package org.acme.quarkus.sample; @@ -215,8 +215,8 @@ import jakarta.ws.rs.core.MediaType; import io.quarkus.qute.TemplateInstance; -@Path("goodbye") -public class GoodbyeResource { +@Path("greeting") +public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) From f4388a12fc92c57c67e7c623838001531a54cdb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:02:55 +0000 Subject: [PATCH 07/38] Bump testcontainers.version from 1.20.3 to 1.20.4 Bumps `testcontainers.version` from 1.20.3 to 1.20.4. Updates `org.testcontainers:testcontainers-bom` from 1.20.3 to 1.20.4 - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.3...1.20.4) Updates `org.testcontainers:testcontainers` from 1.20.3 to 1.20.4 - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.3...1.20.4) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.testcontainers:testcontainers dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 484363a241e73cd510defffd6330072a231b04c2) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 06a640bf68baa..d372873e13037 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -195,7 +195,7 @@ 1.12.0 2.6.5.Final 0.1.18.Final - 1.20.3 + 1.20.4 3.4.0 2.0.2 From 6c1825caa6a429c97e6b5b8ad48d2780ca5d9b9d Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Sat, 16 Nov 2024 09:25:12 -0500 Subject: [PATCH 08/38] Update Docker Config Handling (cherry picked from commit 7b9d7a01cfe81575aaec34ab5d137dbac2c9e655) --- .../buildpack/deployment/BuildpackConfig.java | 13 ++++++++ .../deployment/BuildpackProcessor.java | 32 +++++++++++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java index 645d3260a9ff0..652c5a3cf99ee 100644 --- a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java +++ b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java @@ -72,6 +72,19 @@ public class BuildpackConfig { @ConfigItem public Optional dockerHost; + /** + * use Daemon mode? + * Defaults to 'true' + */ + @ConfigItem(defaultValue = "true") + public Boolean useDaemon; + + /** + * Use specified docker network during build + */ + @ConfigItem + public Optional dockerNetwork; + /** * Log level to use.. * Defaults to 'info' diff --git a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java index 9efe801b87995..3f4c1c210c32c 100644 --- a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java +++ b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java @@ -154,6 +154,24 @@ private Map getPaths(OutputTargetBuildItem outputTarget) { return result; } + private final String getDockerHost(BuildpackConfig buildpackConfig) { + String dockerHostVal = null; + //use config if present, else try to use env var. + //use of null indicates to buildpack lib to default the value itself. + if (buildpackConfig.dockerHost.isPresent()) { + dockerHostVal = buildpackConfig.dockerHost.get(); + } else { + String dockerHostEnv = System.getenv("DOCKER_HOST"); + if (dockerHostEnv != null && !dockerHostEnv.isEmpty()) { + dockerHostVal = dockerHostEnv; + } + } + if (dockerHostVal != null) { + log.info("Using dockerHost of " + dockerHostVal); + } + return dockerHostVal; + } + private String runBuildpackBuild(BuildpackConfig buildpackConfig, ContainerImageInfoBuildItem containerImage, ContainerImageConfig containerImageConfig, @@ -196,6 +214,9 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, .withPullRetryIncreaseSeconds(buildpackConfig.pullTimeoutIncreaseSeconds) .withPullTimeoutSeconds(buildpackConfig.pullTimeoutSeconds) .withPullRetryCount(buildpackConfig.pullRetryCount) + .withDockerHost(getDockerHost(buildpackConfig)) + .withDockerNetwork(buildpackConfig.dockerNetwork.orElse(null)) + .withUseDaemon(buildpackConfig.useDaemon) .endDockerConfig() .accept(BuildConfigBuilder.class, b -> { if (isNativeBuild) { @@ -209,12 +230,6 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, b.withRunImage(new ImageReference(buildpackConfig.runImage.get())); } - if (buildpackConfig.dockerHost.isPresent()) { - log.info("Using DockerHost of " + buildpackConfig.dockerHost.get()); - b.editDockerConfig().withDockerHost(buildpackConfig.dockerHost.get()) - .endDockerConfig(); - } - if (buildpackConfig.trustBuilderImage.isPresent()) { log.info("Setting trusted image to " + buildpackConfig.trustBuilderImage.get()); b.editPlatformConfig().withTrustBuilder(buildpackConfig.trustBuilderImage.get()) @@ -244,9 +259,8 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, log.info("Pushing image to " + authConfig.getRegistryAddress()); Stream.concat(Stream.of(containerImage.getImage()), containerImage.getAdditionalImageTags().stream()).forEach(i -> { - //If no dockerHost is specified use empty String. The util will take care of the rest. - String dockerHost = buildpackConfig.dockerHost.orElse(""); - ResultCallback.Adapter callback = DockerClientUtils.getDockerClient(dockerHost) + ResultCallback.Adapter callback = DockerClientUtils + .getDockerClient(getDockerHost(buildpackConfig)) .pushImageCmd(i).start(); try { callback.awaitCompletion(); From 61530f295a0decc0f80c736825b484a7e3276f30 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Wed, 20 Nov 2024 12:38:32 -0500 Subject: [PATCH 09/38] Copyedits for style (cherry picked from commit ea3f56ae23224835f664dbd2cc509f751c1564db) --- docs/src/main/asciidoc/security-jwt.adoc | 436 ++++++++++++----------- 1 file changed, 225 insertions(+), 211 deletions(-) diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index fcc6ab1b97ea2..3d869ba604326 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -7,20 +7,23 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using JWT RBAC include::_attributes.adoc[] :categories: security -:summary: This guide explains how your application can utilize SmallRye JWT to provide secured access to the Jakarta REST endpoints. +:summary: This guide explains how your application can use SmallRye JWT to provide secured access to the Jakarta REST endpoints. :extension-name: SmallRye JWT :mp-jwt: MicroProfile JWT RBAC :topics: security,jwt :extensions: io.quarkus:quarkus-smallrye-jwt -This guide explains how your Quarkus application can utilize https://github.com/smallrye/smallrye-jwt/[SmallRye JWT] -to verify https://tools.ietf.org/html/rfc7519[JSON Web Token]s, represent them as MicroProfile JWT `org.eclipse.microprofile.jwt.JsonWebToken` -and provide secured access to the Quarkus HTTP endpoints using Bearer Token Authorization and https://en.wikipedia.org/wiki/Role-based_access_control[Role-Based Access Control]. +This guide explains how to integrate link:https://github.com/smallrye/smallrye-jwt/[SmallRye JWT] into your Quarkus application to implement link:https://tools.ietf.org/html/rfc7519[JSON Web Token (JWT)] security in compliance with the MicroProfile JWT specification. +You’ll learn how to verify JWTs, represent them as MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken, and secure Quarkus HTTP endpoints using bearer token authorization and link:https://en.wikipedia.org/wiki/Role-based_access_control[Role-Based Access Control]. -NOTE: Quarkus OpenID Connect `quarkus-oidc` extension also supports Bearer Token Authorization and uses `smallrye-jwt` to represent the bearer tokens as `JsonWebToken`. -For more information, read the xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication] guide. -OpenID Connect extension has to be used if the Quarkus application needs to authenticate the users using OIDC Authorization Code Flow. -For more information, see xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] +[NOTE] +==== +The Quarkus OpenID Connect (`quarkus-oidc`) extension also supports bearer token authorization and uses `smallrye-jwt` to represent bearer tokens as `JsonWebToken`. +For details, see the xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer Token Authentication] guide. + +If your Quarkus application needs to authenticate users using the OIDC Authorization Code Flow, you must use the OpenID Connect extension. +For more information, refer to the xref:security-oidc-code-flow-authentication.adoc[OIDC Code Flow Mechanism for Protecting Web Applications]. +==== == Prerequisites @@ -30,12 +33,15 @@ include::{includes}/prerequisites.adoc[] === Solution -We recommend that you follow the instructions in the next sections and create the application step by step. -However, you can skip right to the completed example. +We recommend following the instructions in the upcoming sections to create the application step by step. +If you prefer, you can skip ahead to the completed example. -Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. +To access the example, either clone the Git repository or download an archive: -The solution is located in the `security-jwt-quickstart` link:{quickstarts-tree-url}/security-jwt-quickstart[directory]. +- Clone the repository: `git clone {quickstarts-clone-url}`. +- Download the {quickstarts-archive-url}[archive]. + +The completed solution is located in the `security-jwt-quickstart` link:{quickstarts-tree-url}/security-jwt-quickstart[directory]. === Creating the Maven project @@ -47,13 +53,12 @@ include::{includes}/devtools/create-app.adoc[] This command generates the Maven project and imports the `smallrye-jwt` extension, which includes the {mp-jwt} support. -If you already have your Quarkus project configured, you can add the `smallrye-jwt` extension -to your project by running the following command in your project base directory: +If you already have your Quarkus project configured, you can add the `smallrye-jwt` extension to your project by running the following command in your project base directory: :add-extension-extensions: smallrye-jwt,smallrye-jwt-build include::{includes}/devtools/extension-add.adoc[] -This will add the following to your build file: +This command adds the following dependencies to your build file: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml @@ -79,7 +84,7 @@ implementation("io.quarkus:quarkus-smallrye-jwt-build") Create a REST endpoint in `src/main/java/org/acme/security/jwt/TokenSecuredResource.java` with the following content: -.REST Endpoint V1 +.REST endpoint V1 [source,java] ---- package org.acme.security.jwt; @@ -131,24 +136,23 @@ public class TokenSecuredResource { } } ---- -<1> Here we inject the JsonWebToken interface, an extension of the java.security.Principal -interface that provides access to the claims associated with the current authenticated token. -<2> @PermitAll is a Jakarta common security annotation that indicates that the given endpoint is accessible by any caller, authenticated or not. -<3> Here we inject the Jakarta REST SecurityContext to inspect the security state of the call and use a `getResponseString()` function to populate a response string. -<4> Here we check if the call is insecure by checking the request user/caller `Principal` against null. -<5> Here we check that the Principal and JsonWebToken have the same name since JsonWebToken does represent the current Principal. -<6> Here we get the Principal name. -<7> The reply we build up makes use of the caller name, the `isSecure()` and `getAuthenticationScheme()` states of the request `SecurityContext`, and whether a non-null `JsonWebToken` was injected. +<1> The `JsonWebToken` interface is injected, providing access to claims associated with the current authenticated token. This interface extends `java.security.Principal`. +<2> The `@PermitAll` is a standard Jakarta security annotation. It indicates that the given endpoint is accessible by all callers, whether authenticated or not. +<3> The Jakarta REST `SecurityContext` is injected to inspect the security state of the request. The `getResponseString()` function generates the response. +<4> Checks if the call is insecure by checking if the request user/caller `Principal` against null. +<5> Ensures the names in the `Principal` and `JsonWebToken` match because the `JsonWebToken` represents the current `Principal`. +<6> Retrieves the name of the `Principal`. +<7> Builds a response containing the caller's name, the `isSecure()` and `getAuthenticationScheme()` states of the request `SecurityContext`, and whether a non-null `JsonWebToken` was injected. === Run the application -Now we are ready to run our application. Use: +Now you are ready to run our application. Use: include::{includes}/devtools/dev.adoc[] -and you should see output similar to: +Then, you should see output similar to the following example: -.quarkus:dev Output +.`quarkus:dev` output [source,shell] ---- [INFO] Scanning for projects... @@ -163,28 +167,28 @@ Listening for transport dt_socket at address: 5005 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, rest, rest-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web] ---- -Now that the REST endpoint is running, we can access it using a command line tool like curl: +Now that the REST endpoint is running, you can access it by using a command line tool such as curl: -.curl command for /secured/permit-all +.`curl` command for `/secured/permit-all` [source,shell] ---- $ curl http://127.0.0.1:8080/secured/permit-all; echo hello anonymous, isHttps: false, authScheme: null, hasJWT: false ---- -We have not provided any JWT in our request, so we would not expect that there is any security state seen by the endpoint, -and the response is consistent with that: +You have not provided any JWT in our request, so you would not expect the endpoint to see any security state, and the response is consistent with that: -* username is anonymous -* isHttps is false as https is not used -* authScheme is null -* hasJWT is false +* `username` is anonymous. +* `isHttps` is `false` because `https` is not used. +* `authScheme` is `null`. +* `hasJWT` is `false`. Use Ctrl-C to stop the Quarkus server. -So now let's actually secure something. Take a look at the new endpoint method `helloRolesAllowed` in the following: +So now let's actually secure something. +Take a look at the new endpoint method `helloRolesAllowed` in the following: -.REST Endpoint V2 +.REST endpoint V2 [source,java] ---- package org.acme.security.jwt; @@ -245,17 +249,19 @@ public class TokenSecuredResource { } } ---- -<1> Here we inject `JsonWebToken` -<2> This new endpoint will be located at /secured/roles-allowed -<3> `@RolesAllowed` is a Jakarta common security annotation that indicates that the given endpoint is accessible by a caller if -they have either a "User" or "Admin" role assigned. -<4> Here we build the reply the same way as in the `hello` method but also add a value of the JWT `birthdate` claim by directly calling the injected `JsonWebToken`. +<1> The `JsonWebToken` is injected to access claims from the JWT. +<2> This endpoint is exposed at `/secured/roles-allowed`. +<3> The `@RolesAllowed` annotation restricts access to users with either the "User" or "Admin" role. +<4> The response is constructed similarly to the `hello` method, with the addition of the `birthdate` claim retrieved directly from the injected `JsonWebToken`. + + + After you make this addition to your `TokenSecuredResource`, rerun the `./mvnw quarkus:dev` command, and then try `curl -v http://127.0.0.1:8080/secured/roles-allowed; echo` to attempt to access the new endpoint. Your output should be as follows: -.curl command for /secured/roles-allowed +.`curl` command for `/secured/roles-allowed` [source,shell] ---- $ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo @@ -276,14 +282,17 @@ $ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo * Connection #0 to host 127.0.0.1 left intact ---- -Excellent, we have not provided any JWT in the request, so we should not be able to access the endpoint, and we were not. Instead, we received an HTTP 401 Unauthorized error. -We need to obtain and pass in a valid JWT to access that endpoint. There are two steps to this, 1) configuring our {extension-name} extension with information on how to validate a JWT, and 2) generating a matching JWT with the appropriate claims. +Excellent. +You have not provided any JWT in the request, so you should not be able to access the endpoint, and you were not able to. +Instead, you received an HTTP 401 Unauthorized error. +You need to obtain and pass in a valid JWT to access that endpoint. +There are two steps to this, 1) configuring our {extension-name} extension with information on how to validate a JWT, and 2) generating a matching JWT with the appropriate claims. -=== Configuring the {extension-name} Extension Security Information +=== Configuring the {extension-name} extension security information Create a `security-jwt-quickstart/src/main/resources/application.properties` with the following content: -.application.properties for TokenSecuredResource +.Application properties for `TokenSecuredResource` [source, properties] ---- mp.jwt.verify.publickey.location=publicKey.pem #<1> @@ -291,23 +300,22 @@ mp.jwt.verify.issuer=https://example.com/issuer #<2> quarkus.native.resources.includes=publicKey.pem #<3> ---- -<1> We are setting public key location to point to a classpath `publicKey.pem` location. We will add this key in part B, <>. -<2> We are setting the issuer to the URL string `https://example.com/issuer`. -<3> We are including the public key as a resource in the native executable. +<1> Specifies the location of the public key file `publicKey.pem` on the classpath. +See <> for adding this key. +<2> Defines the expected issuer as `https://example.com/issuer`. +<3> Ensures the `publicKey.pem` file is included as a resource in the native executable. [[add-public-key]] -=== Adding a Public Key - -The https://tools.ietf.org/html/rfc7519[JWT specification] defines various levels of security of JWTs that one can use. -The {mp-jwt} specification requires that JWTs that are signed with the RSA-256 signature algorithm. This in -turn requires an RSA public key pair. -On the REST endpoint server side, you need to configure the location of the RSA public -key to use to verify the JWT sent along with requests. -The `mp.jwt.verify.publickey.location=publicKey.pem` setting configured -previously expects that the public key is available on the classpath as `publicKey.pem`. +=== Adding a public key + +The link:https://tools.ietf.org/html/rfc7519[JWT specification] defines various levels of security of JWTs that one can use. +The {mp-jwt} specification requires JWTs signed with the RSA-256 signature algorithm. +This in turn requires an RSA public key pair. +On the REST endpoint server side, you need to configure the location of the RSA public key to use to verify the JWT sent along with requests. +The `mp.jwt.verify.publickey.location=publicKey.pem` setting configured previously expects that the public key is available on the classpath as `publicKey.pem`. To accomplish this, copy the following content to a `security-jwt-quickstart/src/main/resources/publicKey.pem` file. -.RSA Public Key PEM Content +.RSA public key PEM content [source, text] ---- -----BEGIN PUBLIC KEY----- @@ -323,12 +331,13 @@ nQIDAQAB === Generating a JWT -Often one obtains a JWT from an identity manager like https://www.keycloak.org/[Keycloak], but for this quickstart we will generate our own using the JWT generation API provided by `smallrye-jwt`. +Often, one obtains a JWT from an identity manager such as link:https://www.keycloak.org/[Keycloak]. +But for this quickstart, you generate our own by using the JWT generation API provided by `smallrye-jwt`. For more information, see xref:security-jwt-build.adoc[Generate JWT tokens with SmallRye JWT]. -Take the code from the following listing and place into `security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java`: +Take the code from the following listing and place it into `security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java`: -.GenerateToken main Driver Class +.`GenerateToken` main driver class [source, java] ---- package org.acme.security.jwt; @@ -356,16 +365,17 @@ public class GenerateToken { } ---- -<1> The `iss` claim is the issuer of the JWT. This needs to match the server side `mp.jwt.verify.issuer`. -in order for the token to be accepted as valid. -<2> The `upn` claim is defined by the {mp-jwt} spec as preferred claim to use for the -`Principal` seen via the container security APIs. +<1> The `iss` claim is the issuer of the JWT. +This must match the server side `mp.jwt.verify.issuer` for the token to be accepted as valid. +<2> The `upn` claim is defined by the {mp-jwt} spec as the preferred claim to use for the `Principal` seen by the container security APIs. <3> The `group` claim provides the groups and top-level roles associated with the JWT bearer. -<4> The `birthday` claim. It can be considered to be a sensitive claim, so you may want to consider encrypting the claims, see xref:security-jwt-build.adoc[Generate JWT tokens with SmallRye JWT]. +<4> The `birthday` claim. +It can be considered a sensitive claim, so consider encrypting the claims, as described in xref:security-jwt-build.adoc[Generate JWT tokens with SmallRye JWT]. -Note for this code to work we need the content of the RSA private key that corresponds to the public key we have in the TokenSecuredResource application. Take the following PEM content and place it into `security-jwt-quickstart/src/test/resources/privateKey.pem`: +Note that for this code to work, you need the content of the RSA private key corresponding to the public key you have in the `TokenSecuredResource` application. +Take the following PEM content and place it into `security-jwt-quickstart/src/test/resources/privateKey.pem`: -.RSA Private Key PEM Content +.RSA private key PEM content [source, text] ---- -----BEGIN PRIVATE KEY----- @@ -398,36 +408,37 @@ f3cg+fr8aou7pr9SHhJlZCU= -----END PRIVATE KEY----- ---- -We will use a `smallrye.jwt.sign.key.location` property to point to this private signing key. +Later, you configure the `smallrye.jwt.sign.key.location` property to specify the location of the private signing key. [NOTE] -.Generating Keys with OpenSSL +.Generating keys with OpenSSL ==== -It is also possible to generate a public and private key pair using the OpenSSL command line tool. +It is also possible to generate a public and private key pair by using the OpenSSL command line tool. -.openssl commands for generating keys +.`openssl` commands to generate keys [source, text] ---- openssl genrsa -out rsaPrivateKey.pem 2048 openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem ---- -An additional step is needed for generating the private key for converting it into the PKCS#8 format. +An additional step is required to generate and convert the private key to the PKCS#8 format, commonly used for secure key storage and transport. -.openssl command for converting private key -[source, text] +.`openssl` commands to perform the conversion +[source, bash] ---- openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem ---- -You can use the generated pair of keys instead of the keys used in this quickstart. +You can use the generated key pair instead of those used in this quickstart. ==== -Now we can generate a JWT to use with `TokenSecuredResource` endpoint. To do this, run the following command: +Now, you can generate a JWT to use with the `TokenSecuredResource` endpoint. +To do this, run the following command: -.Command to Generate JWT +.Command to generate JWT -.Sample JWT Generation Output +.Sample JWT generation output [source,shell] ---- $ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem @@ -435,44 +446,44 @@ $ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.clas eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjU5Njc2LCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1OTk3NiwiaWF0IjoxNTUxNjU5Njc2LCJqdGkiOiJhLTEyMyJ9.O9tx_wNNS4qdpFhxeD1e7v4aBNWz1FCq0UV8qmXd7dW9xM4hA5TO-ZREk3ApMrL7_rnX8z81qGPIo_R8IfHDyNaI1SLD56gVX-NaOLS2OjfcbO3zOWJPKR_BoZkYACtMoqlWgIwIRC-wJKUJU025dHZiNL0FWO4PjwuCz8hpZYXIuRscfFhXKrDX1fh3jDhTsOEFfu67ACd85f3BdX9pe-ayKSVLh_RSbTbBPeyoYPE59FW7H5-i8IE-Gqu838Hz0i38ksEJFI25eR-AJ6_PSUD0_-TV3NjXhF3bFIeT4VSaIZcpibekoJg0cQm-4ApPEcPLdgTejYHA-mupb8hSwg ---- -The JWT string is the Base64 URL encoded string that has 3 parts separated by '.' characters. +The JWT string is a Base64 URL encoded string with three parts separated by '.' characters. First part - JWT headers, second part - JWT claims, third part - JWT signature. -=== Finally, Secured Access to /secured/roles-allowed -Now let's use this to make a secured request to the /secured/roles-allowed endpoint. Make sure you have the Quarkus server still running in dev mode, and then run the following command, making sure to use your version of the generated JWT from the previous step: +=== Finally, secured access to `/secured/roles-allowed` + +Now, let's use this to make a secured request to the `/secured/roles-allowed` endpoint. +Make sure you have the Quarkus server still running in dev mode, and then run the following command, making sure to use your version of the generated JWT from the previous step: [source,bash] ---- curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed; echo ---- -.curl Command for /secured/roles-allowed With JWT +.`curl` command for `/secured/roles-allowed` with JWT [source,shell] ---- $ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13 ---- -Success! We now have: +Success! You now have the following: -* a non-anonymous caller name of jdoe@quarkus.io -* an authentication scheme of Bearer -* a non-null JsonWebToken -* birthdate claim value +- A non-anonymous caller name: `jdoe@quarkus.io` +- An authentication scheme: `Bearer` +- A non-null `JsonWebToken` +- The `birthdate` claim value -=== Using the JsonWebToken and Claim Injection +=== Using the `JsonWebToken` and claim injection -Now that we can generate a JWT to access our secured REST endpoints, let's see what more we can do with the `JsonWebToken` -interface and the JWT claims. The `org.eclipse.microprofile.jwt.JsonWebToken` interface extends the `java.security.Principal` -interface, and is in fact the type of the object that is returned by the `jakarta.ws.rs.core.SecurityContext#getUserPrincipal()` call we -used previously. This means that code that does not use CDI but does have access to the REST container `SecurityContext` can get -hold of the caller `JsonWebToken` interface by casting the `SecurityContext#getUserPrincipal()`. +Now that you can generate a JWT to access our secured REST endpoints, let's see what more you can do with the `JsonWebToken` interface and the JWT claims. +The `org.eclipse.microprofile.jwt.JsonWebToken` interface extends the `java.security.Principal` interface, and is the object type returned by the `jakarta.ws.rs.core.SecurityContext#getUserPrincipal()` call you used previously. +This means that code that does not use CDI but does have access to the REST container `SecurityContext` can get hold of the caller `JsonWebToken` interface by casting the `SecurityContext#getUserPrincipal()`. The `JsonWebToken` interface defines methods for accessing claims in the underlying JWT. -It provides accessors for common claims that are required by the {mp-jwt} specification as well as arbitrary claims that may exist in the JWT. +It provides accessors for common claims that are required by the {mp-jwt} specification and arbitrary claims that might exist in the JWT. All the JWT claims can also be injected. -Let's expand our `TokenSecuredResource` with another endpoint /secured/roles-allowed-admin which uses the injected `birthdate` claim (as opposed to getting it from `JsonWebToken`): +Let's expand our `TokenSecuredResource` with another endpoint `/secured/roles-allowed-admin` which uses the injected `birthdate` claim (as opposed to getting it from `JsonWebToken`): [source, java] ---- @@ -549,10 +560,10 @@ public class TokenSecuredResource { } } ---- -<1> `RequestScoped` scope is required to support an injection of the `birthday` claim as `String`. -<2> Here we inject the JsonWebToken. -<3> Here we inject the `birthday` claim as `String` - this is why the `@RequestScoped` scope is now required. -<4> Here we use the injected `birthday` claim to build the final reply. +<1> The `@RequestScoped` scope is required to enable injection of the `birthdate` claim as a `String`. +<2> The `JsonWebToken` is injected here, providing access to all claims and JWT-related information. +<3> The `birthdate` claim is injected as a `String`. This highlights why the `@RequestScoped` scope is mandatory. +<4> The injected `birthdate` claim is directly used to construct the response. Now generate the token again and run: @@ -569,13 +580,13 @@ hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthda === Package and run the application -As usual, the application can be packaged using: +As usual, the application can be packaged by using: include::{includes}/devtools/build.adoc[] -And executed using `java -jar target/quarkus-app/quarkus-run.jar`: +And executed by using `java -jar target/quarkus-app/quarkus-run.jar`: -.Runner jar Example +.Runner jar example [source,shell,subs=attributes+] ---- $ java -jar target/quarkus-app/quarkus-run.jar @@ -587,7 +598,7 @@ You can also generate the native executable with: include::{includes}/devtools/build-native.adoc[] -.Native Executable Example +.Native executable example [source,shell] ---- [INFO] Scanning for projects... @@ -612,15 +623,16 @@ $ ./target/security-jwt-quickstart-runner 2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, smallrye-jwt] ---- -=== Explore the Solution +=== Explore the solution + +The `security-jwt-quickstart` link:{quickstarts-tree-url}/security-jwt-quickstart[directory] repository contains all the versions covered in this quickstart guide, along with additional endpoints that demonstrate subresources using injected `JsonWebToken`s and their claims via CDI APIs. -The solution repository located in the `security-jwt-quickstart` link:{quickstarts-tree-url}/security-jwt-quickstart[directory] contains all the versions we have worked through in this quickstart guide as well as some additional endpoints that illustrate subresources with injection of ``JsonWebToken``s and their claims into those using the CDI APIs. -We suggest that you check out the quickstart solutions and explore the `security-jwt-quickstart` directory to learn more about the {extension-name} extension features. +We encourage you to explore the `security-jwt-quickstart` directory and review the quickstart solutions to learn more about the features of the {extension-name} extension. -== Reference Guide +== Reference guide [[supported-injection-scopes]] -=== Supported Injection Scopes +=== Supported injection scopes `@ApplicationScoped`, `@Singleton` and `@RequestScoped` outer bean injection scopes are all supported when an `org.eclipse.microprofile.jwt.JsonWebToken` is injected, with the `@RequestScoped` scoping for `JsonWebToken` enforced to ensure the current token is represented. @@ -644,15 +656,14 @@ public class TokenSecuredResource { } ---- -Note you can also use the injected `JsonWebToken` to access the individual claims in which case setting `@RequestScoped` is not necessary. +Note you can also use the injected `JsonWebToken` to access the individual claims, but setting `@RequestScoped` is unnecessary in this case. Please see link:https://download.eclipse.org/microprofile/microprofile-jwt-auth-1.2/microprofile-jwt-auth-spec-1.2.html#_cdi_injection_requirements[MP JWT CDI Injection Requirements] for more details. [[supported-public-key-formats]] -=== Supported Public Key Formats +=== Supported public key formats -Public Keys may be formatted in any of the following formats, specified in order of -precedence: +Public keys can be formatted in any of the following formats, specified in order of precedence: - Public Key Cryptography Standards #8 (PKCS#8) PEM - JSON Web Key (JWK) @@ -660,13 +671,13 @@ precedence: - JSON Web Key (JWK) Base64 URL encoded - JSON Web Key Set (JWKS) Base64 URL encoded -=== Dealing with the verification keys +=== Dealing with verification keys -If you need to verify the token signature using the asymmetric RSA or Elliptic Curve (EC) key then use the `mp.jwt.verify.publickey.location` property to refer to the local or remote verification key. +If you need to verify the token signature by using the asymmetric RSA or Elliptic Curve (EC) key, use the `mp.jwt.verify.publickey.location` property to refer to the local or remote verification key. -Use `mp.jwt.verify.publickey.algorithm` to customize the verification algorithm (default is `RS256`), for example, set it to `ES256` when working with the EC keys. +Use `mp.jwt.verify.publickey.algorithm` to customize the verification algorithm (default is `RS256`); for example, set it to `ES256` when working with the EC keys. -If you need to verify the token signature using the symmetric secret key then either a `JSON Web Key` (JWK) or `JSON Web Key Set` (JWK Set) format must be used to represent this secret key, for example: +If you need to verify the token signature by using the symmetric secret key, then either a `JSON Web Key` (JWK) or `JSON Web Key Set` (JWK Set) format must be used to represent this secret key, for example: [source,json] ---- @@ -681,11 +692,11 @@ If you need to verify the token signature using the symmetric secret key then ei } ---- -This secret key JWK will also need to be referred to with `smallrye.jwt.verify.key.location`. +This secret key JWK must also be referred to with `smallrye.jwt.verify.key.location`. `smallrye.jwt.verify.algorithm` should be set to `HS256`/`HS384`/`HS512`. [[jwt-parser]] -=== Parse and Verify JsonWebToken with JWTParser +=== Parse and verify `JsonWebToken` with `JWTParser` If the JWT token can not be injected, for example, if it is embedded in the service request payload or the service endpoint acquires it out of band, then one can use `JWTParser`: @@ -702,7 +713,8 @@ String token = getTokenFromOidcServer(); JsonWebToken jwt = parser.parse(token); ---- -You can also use it to customize the way the token is verified or decrypted. For example, one can supply a local `SecretKey`: +You can also use it to customize how the token is verified or decrypted. +For example, one can supply a local `SecretKey`: [source,java] ---- @@ -731,13 +743,13 @@ public class SecuredResource { @Produces("text/plain") public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException { if (jwtCookie == null) { - // Create a JWT token signed using the 'HS256' algorithm + // Create a JWT token signed by using the 'HS256' algorithm String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET); - // or create a JWT token encrypted using the 'A256KW' algorithm + // or create a JWT token encrypted by using the 'A256KW' algorithm // Jwt.upn("alice").encryptWithSecret(secret); return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build(); } else { - // All mp.jwt and smallrye.jwt properties are still effective, only the verification key is customized. + // All mp.jwt and smallrye.jwt properties are still effective; only the verification key is customized. JsonWebToken jwt = parser.verify(jwtCookie, SECRET); // or jwt = parser.decrypt(jwtCookie, secret); return Response.ok(jwt.getName()).build(); @@ -748,22 +760,23 @@ public class SecuredResource { Please also see the <> section about using `JWTParser` without the `HTTP` support provided by `quarkus-smallrye-jwt`. -=== Token Decryption +=== Token decryption + +If your application needs to accept tokens with encrypted claims or encrypted inner-signed claims, simply set the `smallrye.jwt.decrypt.key.location` property to point to the decryption key. -If your application needs to accept the tokens with the encrypted claims or the encrypted inner-signed claims, all you have to do is set -`smallrye.jwt.decrypt.key.location` pointing to the decryption key. +If this is the only key property set, the incoming token is expected to contain only encrypted claims. +If either `mp.jwt.verify.publickey` or `mp.jwt.verify.publickey.location` verification properties are also set, then the incoming token is expected to contain the encrypted inner-signed token. -If this is the only key property that is set, the incoming token is expected to contain the encrypted claims only. -If either `mp.jwt.verify.publickey` or `mp.jwt.verify.publickey.location` verification properties are also set then the incoming token is expected to contain the encrypted inner-signed token. +See xref:security-jwt-build.adoc[Generate JWT tokens with SmallRye JWT] and learn how to generate the encrypted or inner-signed and then encrypted tokens quickly. -See xref:security-jwt-build.adoc[Generate JWT tokens with SmallRye JWT] and learn how to generate the encrypted or inner-signed and then encrypted tokens fast. +=== Custom factories -=== Custom Factories +The `io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory` is the default implementation used to parse and verify JWT tokens, converting them into `JsonWebToken` principals. This factory relies on the `MP JWT` and `smallrye-jwt` properties, as described in the `Configuration` section, to validate and customize JWT tokens. -`io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory` is used by default to parse and verify JWT tokens and convert them to `JsonWebToken` principals. -It uses `MP JWT` and `smallrye-jwt` properties listed in the `Configuration` section to verify and customize JWT tokens. +If you need to implement a custom factory—such as to skip re-verifying tokens that have already been validated by a firewall—you can do so in one of the following ways: -If you need to provide your own factory, for example, to avoid verifying the tokens again which have already been verified by the firewall, then you can either use a `ServiceLoader` mechanism by providing a `META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory` resource or simply have an `Alternative` CDI bean implementation like this one: +- Use the `ServiceLoader` mechanism by creating a `META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory` resource. +- Provide an `Alternative` CDI bean implementation, like the example below: [source,java] ---- @@ -788,7 +801,7 @@ public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory { @Override public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException { try { - // Token has already been verified, parse the token claims only + // Token has already been verified; parse the token claims only String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); return new DefaultJWTCallerPrincipal(JwtClaims.parse(json)); } catch (InvalidJwtException ex) { @@ -802,16 +815,16 @@ public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory { `quarkus-smallrye-jwt` extension uses link:https://github.com/smallrye/smallrye-jwt[SmallRye JWT] library which is currently not reactive. -What it means from the perspective of `quarkus-smallrye-jwt` which operates as part of the reactive Quarkus security architecture, is that an IO thread entering the link:https://github.com/smallrye/smallrye-jwt[SmallRye JWT] verification or decryption code might block in one of the following cases: +What it means from the perspective of `quarkus-smallrye-jwt`, which operates as part of the reactive Quarkus security architecture, is that an IO thread entering the link:https://github.com/smallrye/smallrye-jwt[SmallRye JWT] verification or decryption code might block in one of the following cases: -* Default key resolver refreshes `JsonWebKey` set containing the keys which involves a remote call to the OIDC endpoint -* Custom key resolver such as `AWS Application Load Balancer` (`ALB`) key resolver, resolves the keys against the AWS ALB key endpoint using the current token's key identifier header value +* The default key resolver refreshes the `JsonWebKey` set containing the keys, which involves a remote call to the OIDC endpoint. +* The custom key resolver, such as `AWS Application Load Balancer` (`ALB`) key resolver, resolves the keys against the AWS ALB key endpoint by using the current token's key identifier header value. -In such cases, if the connections are slow, for example, it may take more than 3 seconds to get a response from the key endpoint, the current event loop thread will most likely block. +In such cases, if connections are slow—for instance, taking more than 3 seconds to respond to the key endpoint—the current event loop thread is likely to become blocked. -To prevent it, set `quarkus.smallrye-jwt.blocking-authentication=true`. +To prevent it from blocking, set `quarkus.smallrye-jwt.blocking-authentication=true`. -=== Token Propagation +=== Token propagation Please see the xref:security-openid-connect-client-reference.adoc#token-propagation-rest[Token Propagation] section about the Bearer access token propagation to the downstream services. @@ -821,7 +834,7 @@ Please see the xref:security-openid-connect-client-reference.adoc#token-propagat [[integration-testing-wiremock]] ==== Wiremock -If you configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#bearer-token-integration-testing[OpenID Connect Bearer Token Integration testing] `Wiremock` section but only change the `application.properties` to use MP JWT configuration properties instead: +If you configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP-based JsonWebKey (JWK) set, then you can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#bearer-token-integration-testing[OpenID Connect Bearer Token Integration testing] `Wiremock` section but only change the `application.properties` to use MP JWT configuration properties instead: [source, properties] ---- @@ -833,7 +846,7 @@ mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus [[integration-testing-keycloak]] ==== Keycloak -If you work with Keycloak and configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#bearer-token-integration-testing[OpenID Connect Bearer Token Integration testing] Keycloak section but only change the `application.properties` to use MP JWT configuration properties instead: +If you work with Keycloak and configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP-based JsonWebKey (JWK) set, you can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#bearer-token-integration-testing[OpenID Connect Bearer Token Integration testing] Keycloak section but only change the `application.properties` to use MP JWT configuration properties instead: [source, properties] ---- @@ -844,14 +857,15 @@ mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus Note that the tokens issued by Keycloak have an `iss` (issuer) claim set to the realm endpoint address. -If your Quarkus application is running in a docker container, it may share a network interface with a Keycloak docker container launched by DevServices for Keycloak, with the Quarkus application and Keycloak communicating with each other via an internal shared docker network. +If your Quarkus application runs in a Docker container, it might share a network interface with a Keycloak container started by DevServices for Keycloak. +In this scenario, the Quarkus application and Keycloak communicate through an internal shared Docker network. In such cases, use the following configuration instead: [source, properties] ---- # keycloak.url is set by DevServices for Keycloak, -# Quarkus will access it via an internal shared docker network interface. +# Quarkus accesses it through an internal shared docker network interface. mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs # Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property @@ -859,9 +873,9 @@ mp.jwt.verify.issuer=${client.quarkus.oidc.auth-server-url} ---- [[integration-testing-public-key]] -==== Local Public Key +==== Local public key -You can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#bearer-token-integration-testing[OpenID Connect Bearer Token Integration testing] `Local Public Key` section but only change the `application.properties` to use MP JWT configuration properties instead: +You can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#bearer-token-integration-testing[OpenID Connect Bearer Token Integration testing] `Local public key` section but only change the `application.properties` to use MP JWT configuration properties instead: [source, properties] ---- @@ -874,7 +888,7 @@ smallrye.jwt.sign.key.location=privateKey.pem ---- [[integration-testing-security-annotation]] -==== TestSecurity annotation +==== `TestSecurity` annotation Add the following dependency: @@ -894,7 +908,7 @@ Add the following dependency: testImplementation("io.quarkus:quarkus-test-security-jwt") ---- -and write a test code like this one: +Then, write test code such as this: [source, java] ---- @@ -931,7 +945,7 @@ public class TestSecurityAuthTest { } ---- -where `ProtectedResource` class may look like this: +where the `ProtectedResource` class might look like this: [source, java] ---- @@ -957,7 +971,7 @@ public class ProtectedResource { } ---- -Note that `@TestSecurity` annotation must always be used and its `user` property is returned as `JsonWebToken.getName()` and `roles` property - as `JsonWebToken.getGroups()`. +Note that the `@TestSecurity` annotation must always be used, and its `user` property is returned as `JsonWebToken.getName()` and `roles` property - as `JsonWebToken.getGroups()`. `@JwtSecurity` annotation is optional and can be used to set the additional token claims. [TIP] @@ -992,14 +1006,14 @@ quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE ---- -=== Proactive Authentication +=== Proactive authentication If you'd like to skip the token verification when the public endpoint methods are invoked, disable the xref:security-proactive-authentication.adoc[proactive authentication]. -Note that you can't access the injected `JsonWebToken` in the public methods if the token verification has not been done. +Note that you can't access the injected `JsonWebToken` through public methods if token verification has not been done. [[add-smallrye-jwt]] -=== How to Add SmallRye JWT directly +=== How to add SmallRye JWT directly To <>, use `smallrye-jwt` instead of `quarkus-smallrye-jwt` directly for the following situations: @@ -1023,7 +1037,7 @@ Start with adding the `smallrye-jwt` dependency: implementation("io.smallrye:smallrye-jwt") ---- -and update `application.properties` to get all the CDI producers provided by `smallrye-jwt` included as follows: +Then, update `application.properties` to get all the CDI producers provided by `smallrye-jwt` included as follows: [source, properties] ---- @@ -1032,7 +1046,7 @@ quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt ---- [[configuration-reference]] -== Configuration Reference +== Configuration reference === Quarkus configuration @@ -1043,68 +1057,68 @@ include::{generated-dir}/config/quarkus-smallrye-jwt.adoc[opts=optional, levelof [cols="> section. -|mp.jwt.verify.publickey.location|none|Config property allows for an external or internal location of Public Key to be specified. The value may be a relative path or a URL. If the value points to an HTTPS based JWK set then, for it to work in native mode, the `quarkus.ssl.native` property must also be set to `true`, see xref:native-and-ssl.adoc[Using SSL With Native Executables] for more details. -|mp.jwt.verify.publickey.algorithm|`RS256`|List of signature algorithms. Set it to `ES256` to support the Elliptic Curve signature algorithm. -|mp.jwt.decrypt.key.location|none|Config property allows for an external or internal location of Private Decryption Key to be specified. -|mp.jwt.decrypt.key.algorithm|`RSA-OAEP`,`RSA-OAEP-256`|List of decryption algorithms. Set it to `RSA-OAEP-256` to support RSA-OAEP with SHA-256 only. -|mp.jwt.verify.issuer|none|Config property specifies the value of the `iss` (issuer) claim of the JWT that the server will accept as valid. -|mp.jwt.verify.audiences|none|Comma separated list of the audiences that a token `aud` claim may contain. -|mp.jwt.verify.clock.skew|`60`|Clock skew in seconds used during the token expiration and age verification. An expired token is accepted if the current time is within the number of seconds specified by this property after the token expiration time. The default value is 60 seconds. -|mp.jwt.verify.token.age|`none`|Number of seconds that must not elapse since the token `iat` (issued at) time. -|mp.jwt.token.header|`Authorization`|Set this property if another header such as `Cookie` is used to pass the token. -|mp.jwt.token.cookie|none|Name of the cookie containing a token. This property will be effective only if `mp.jwt.token.header` is set to `Cookie`. +|`mp.jwt.verify.publickey`|none|The `mp.jwt.verify.publickey` config property allows the public key text to be supplied as a string. The public key is parsed from the supplied string in the order defined in the <> section. +|`mp.jwt.verify.publickey.location`|none|Config property allows for a specified external or internal location of the public key. The value can be a relative path or a URL. If the value points to an HTTPS-based JWK set, then, for it to work in native mode, the `quarkus.ssl.native` property must also be set to `true`. See xref:native-and-ssl.adoc[Using SSL With Native Executables] for more details. +|`mp.jwt.verify.publickey.algorithm`|`RS256`|List of signature algorithms. Set it to `ES256` to support the Elliptic Curve signature algorithm. +|`mp.jwt.decrypt.key.location`|none|Config property allows for a specified external or internal location of the Private Decryption Key. +|`mp.jwt.decrypt.key.algorithm`|`RSA-OAEP`,`RSA-OAEP-256`|List of decryption algorithms. Set it to `RSA-OAEP-256` to support RSA-OAEP with SHA-256 only. +|`mp.jwt.verify.issuer`|none|Config property specifies the value of the `iss` (issuer) claim of the JWT that the server accepts as valid. +|`mp.jwt.verify.audiences`|none| Comma-separated list of audiences a token `aud` claim might contain. +|`mp.jwt.verify.clock.skew`|`60`|Clock skew in seconds used during the token expiration and age verification. An expired token is accepted if the current time is within the number of seconds specified by this property after the token expiration time. The default value is 60 seconds. +|`mp.jwt.verify.token.age`|`none`|Number of seconds that must not elapse since the token `iat` (issued at) time. +|`mp.jwt.token.header`|`Authorization`|Set this property if another header, such as `Cookie`, is used to pass the token. +|`mp.jwt.token.cookie`|none|Name of the cookie containing a token. This property is effective only if `mp.jwt.token.header` is set to `Cookie`. |=== === Additional SmallRye JWT configuration -SmallRye JWT provides more properties which can be used to customize the token processing: +SmallRye JWT provides more properties that can be used to customize the token processing: [cols=" Date: Thu, 21 Nov 2024 15:05:28 +0200 Subject: [PATCH 10/38] Register method for reflection when read or write interceptors exist This is needed because the interceptors need to get method metadata Fixes: #44564 (cherry picked from commit 314e3d527fdc24a4cf30cf1d028ed3489cafe57b) --- .../server/deployment/ResteasyReactiveProcessor.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 2fd62b912d680..35ce4ab24bdde 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 @@ -1210,7 +1210,14 @@ public void additionalReflection(BeanArchiveIndexBuildItem beanArchiveIndexBuild // when a ContainerResponseFilter exists, it can potentially do responseContext.setEntityStream() // which then forces the use of the slow path for calling writers - if (!resourceInterceptorsBuildItem.getResourceInterceptors().getContainerResponseFilters().isEmpty()) { + ResourceInterceptors resourceInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors(); + if (!resourceInterceptors.getContainerResponseFilters().isEmpty()) { + serializersRequireResourceReflection = true; + } + // when ReaderInterceptor or WriterInterceptor is used, we need to access to the Method + // because of InterceptorContext + if (!(resourceInterceptors.getReaderInterceptors().isEmpty() + && resourceInterceptors.getWriterInterceptors().isEmpty())) { serializersRequireResourceReflection = true; } From c0874b31055d46783a7ee630e4a18b41b832b7e8 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Thu, 21 Nov 2024 11:03:01 -0500 Subject: [PATCH 11/38] Copyedits for style security-jwt-build.adoc (cherry picked from commit b8b2675b283ee16cc6fe612a73279cbe99a38247) --- .../src/main/asciidoc/security-jwt-build.adoc | 217 ++++++++++-------- 1 file changed, 127 insertions(+), 90 deletions(-) diff --git a/docs/src/main/asciidoc/security-jwt-build.adoc b/docs/src/main/asciidoc/security-jwt-build.adoc index 61da04211bcf0..de94662e7a8e9 100644 --- a/docs/src/main/asciidoc/security-jwt-build.adoc +++ b/docs/src/main/asciidoc/security-jwt-build.adoc @@ -3,24 +3,26 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// -= Build, Sign and Encrypt JSON Web Tokens += Build, sign, and encrypt JSON Web Tokens include::_attributes.adoc[] :categories: security :topics: security,jwt :extensions: io.quarkus:quarkus-smallrye-jwt-build -According to link:https://datatracker.ietf.org/doc/html/rfc7519[RFC7519], JSON Web Token (JWT) is a compact, URL-safe means of representing claims which are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code(MAC) and/or encrypted. +JSON Web Token (JWT) is defined by the link:https://datatracker.ietf.org/doc/html/rfc7519[RFC 7519] specification as a compact, URL-safe means of representing claims. These claims are encoded as a JSON object and can be used as the payload of a JSON Web Signature (JWS) structure or the plaintext of a JSON Web Encryption (JWE) structure. This mechanism enables claims to be digitally signed or protected for integrity with a Message Authentication Code (MAC) and encrypted. -Signing the claims is used most often to secure the claims. What is known today as a JWT token is typically produced by signing the claims in a JSON format using the steps described in the link:https://tools.ietf.org/html/rfc7515[JSON Web Signature] specification. +Signing the claims is the most common method for securing them. Typically, a JWT token is produced by signing claims formatted as JSON, following the steps outlined in the link:https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] specification. -However, when the claims are sensitive, their confidentiality can be guaranteed by following the steps described in the link:https://tools.ietf.org/html/rfc7516[JSON Web Encryption] specification to produce a JWT token with the encrypted claims. +When the claims contain sensitive information, their confidentiality can be ensured by using the link:https://tools.ietf.org/html/rfc7516[JSON Web Encryption (JWE)] specification. This approach produces a JWT with encrypted claims. -Finally, both the confidentiality and integrity of the claims can be further enforced by signing them first and then encrypting the nested JWT token. +For enhanced security, you can combine both methods: sign the claims first and then encrypt the resulting nested JWT. This process ensures both the confidentiality and integrity of the claims. -SmallRye JWT Build provides an API for securing JWT claims using all of these options. link:https://bitbucket.org/b_c/jose4j/wiki/Home[Jose4J] is used internally to support this API. +The SmallRye JWT Build API simplifies securing JWT claims by supporting all these options. It uses the link:https://bitbucket.org/b_c/jose4j/wiki/Home[Jose4J] library internally to provide this functionality. == Dependency +To use the SmallRye JWT Build API, add the following dependency to your project: + [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -30,18 +32,19 @@ SmallRye JWT Build provides an API for securing JWT claims using all of these op ---- +Alternatively, for a Gradle-based project, use the following configuration: + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- implementation("io.quarkus:quarkus-smallrye-jwt-build") ---- -Note you can use SmallRye JWT Build API without having to create MicroProfile JWT endpoints supported by `quarkus-smallrye-jwt`. -It can also be excluded from `quarkus-smallrye-jwt` if MP JWT endpoints do not need to generate JWT tokens. +You can use the SmallRye JWT Build API independently, without creating MicroProfile JWT endpoints supported by the `quarkus-smallrye-jwt` extension. == Create JwtClaimsBuilder and set the claims -The first step is to initialize a `JwtClaimsBuilder` using one of the options below and add some claims to it: +The first step is to initialize a `JwtClaimsBuilder` by using one of the following options and add some claims to it: [source, java] ---- @@ -55,117 +58,144 @@ import org.eclipse.microprofile.jwt.JsonWebToken; // Create an empty builder and add some claims JwtClaimsBuilder builder1 = Jwt.claims(); builder1.claim("customClaim", "custom-value").issuer("https://issuer.org"); -// Or start typing the claims immediately: +// Alternatively, start with claims directly: // JwtClaimsBuilder builder1 = Jwt.upn("Alice"); -// Builder created from the existing claims +// Create a builder from an existing claims file JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json"); -// Builder created from a map of claims +// Create a builder from a map of claims JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")); -// Builder created from JsonObject +// Create a builder from a JsonObject JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build(); JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build(); JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build(); JwtClaimsBuilder builder4 = Jwt.claims(json); -// Builder created from JsonWebToken +// Create a builder from a JsonWebToken @Inject JsonWebToken token; JwtClaimsBuilder builder5 = Jwt.claims(token); ---- -The API is fluent so the builder initialization can be done as part of the fluent API sequence. +The API is fluent so you can initialize the builder as part of a fluent sequence. -The builder will also set `iat` (issued at) to the current time, `exp` (expires at) to 5 minutes away from the current time (it can be customized with the `smallrye.jwt.new-token.lifespan` property) and `jti` (unique token identifier) claims if they have not already been set. +The builder automatically sets the following claims if they are not explicitly configured: +- `iat` (issued at): Current time +- `exp` (expires at): Five minutes from the current time (customizable with the `smallrye.jwt.new-token.lifespan` property) +- `jti` (unique token identifier) -One can also configure `smallrye.jwt.new-token.issuer` and `smallrye.jwt.new-token.audience` properties and skip setting the issuer and audience directly with the builder API. +You can configure the following properties globally to avoid setting them directly in the builder: +- `smallrye.jwt.new-token.issuer`: Specifies the default issuer. +- `smallrye.jwt.new-token.audience`: Specifies the default audience. -The next step is to decide how to secure the claims. +After initializing and setting claims, the next step is to decide how to secure the claims. [[sign-claims]] == Sign the claims -The claims can be signed immediately or after the `JSON Web Signature` headers have been set: +You can sign the claims immediately or after configuring the `JSON Web Signature (JWS)` headers: [source, java] ---- import io.smallrye.jwt.build.Jwt; ... -// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. -// No 'jws()' transition is necessary. Default algorithm is RS256. +// Sign the claims using an RSA private key loaded from the location specified by the 'smallrye.jwt.sign.key.location' property. +// No 'jws()' transition is required. The default algorithm is RS256. String jwt1 = Jwt.claims("/tokenClaims.json").sign(); -// Set the headers and sign the claims with an RSA private key loaded in the code (the implementation of this method is omitted). -// Note a 'jws()' transition to a 'JwtSignatureBuilder', Default algorithm is RS256. -String jwt2 = Jwt.claims("/tokenClaims.json").jws().keyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey()); +// Set the headers and sign the claims by using an RSA private key loaded in the code (the implementation of this method is omitted). +// Includes a 'jws()' transition to a 'JwtSignatureBuilder'. The default algorithm is RS256. + +String jwt2 = Jwt.claims("/tokenClaims.json") + .jws() + .keyId("kid1") + .header("custom-header", "custom-value") + .sign(getPrivateKey()); ---- -Note the `alg` (algorithm) header is set to `RS256` by default. Signing key identifier (`kid` header) does not have to be set if a single JSON Web Key (JWK) containing a `kid` property is used. +Default behaviors: + +- The `alg` (algorithm) header is set to `RS256` by default. +- You do not have to set a signing key identifier (`kid` header) if a single JSON Web Key (JWK) containing a `kid` property is used. -RSA and Elliptic Curve (EC) private keys as well as symmetric secret keys can be used to sign the claims. -`ES256` and `HS256` are the default algorithms for EC private and symmetric key algorithms respectively. +Supported keys and algorithms: -You can customize the signature algorithm, for example: +- To sign the claims, you can use RSA private keys, Elliptic Curve (EC) private keys, and symmetric secret keys. +- `RS256` is the default RSA private key signature algorithm. +- `ES256` is the default EC private key signature algorithm. +- `HS256` is the default symmetric key signature algorithm. + +To customize the signature algorithm, use the `JwtSignatureBuilder` API. For example: [source, java] ---- import io.smallrye.jwt.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; -// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. Algorithm is PS256. +// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. The algorithm is PS256. String jwt = Jwt.upn("Alice").jws().algorithm(SignatureAlgorithm.PS256).sign(); ---- -Alternatively you can use a `smallrye.jwt.new-token.signature-algorithm` property: +Alternatively, you can configure the signature algorithm globally with the following property: [source,properties] ---- smallrye.jwt.new-token.signature-algorithm=PS256 ---- -and write a simpler API sequence: +This approach gives you a simpler API sequence: [source, java] ---- import io.smallrye.jwt.build.Jwt; -// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. Algorithm is PS256. +// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. The algorithm is PS256. String jwt = Jwt.upn("Alice").sign(); ---- -Note the `sign` step can be combined with the <> step to produce `inner-signed and encrypted` tokens, see <> section. +You can combine the `sign` step with the <> step to create `inner-signed and encrypted` tokens. For more information, see the <> section. [[encrypt-claims]] == Encrypt the claims -The claims can be encrypted immediately or after the `JSON Web Encryption` headers have been set the same way as they can be signed. -The only minor difference is that encrypting the claims always requires a `jwe()` `JwtEncryptionBuilder` transition given that the API has been optimized to support signing and inner-signing of the claims. +You can encrypt claims immediately or after setting the `JSON Web Encryption (JWE)` headers, similar to how claims are signed. +However, encrypting claims always requires a `jwe()` transition to a `JwtEncryptionBuilder` because the API is optimized to support signing and inner-signing operations. [source, java] ---- import io.smallrye.jwt.build.Jwt; ... -// Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. Default key encryption algorithm is RSA-OAEP. +// Encrypt the claims using an RSA public key loaded from the location specified by the 'smallrye.jwt.encrypt.key.location' property. +// The default key encryption algorithm is RSA-OAEP. + String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt(); -// Set the headers and encrypt the claims with an RSA public key loaded in the code (the implementation of this method is omitted). Default key encryption algorithm is A256KW. +// Set the headers and encrypt the claims by using an RSA public key loaded in the code (the implementation of this method is omitted). +// The default key encryption algorithm is A256KW. String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getSecretKey()); ---- -Note the `alg` (key management algorithm) header is set to `RSA-OAEP` and the `enc` (content encryption header) is set to `A256GCM` by default. +Default behaviors: + +- The `alg` (key management algorithm) header defaults to `RSA-OAEP`. +- The `enc` (content encryption) header defaults to `A256GCM`. -RSA and Elliptic Curve (EC) public keys as well as symmetric secret keys can be used to encrypt the claims. -`ECDH-ES` and `A256KW` are the default algorithms for EC public and symmetric key encryption algorithms respectively. +Supported keys and algorithms: + +- You can use RSA public keys, Elliptic Curve (EC) public keys, and symmetric secret keys, to encrypt the claims. +- `RSA-OAEP` is the default RSA public key encryption algorithm. +- `ECDH-ES` is the default EC public key encryption algorithm. +- `A256KW` is the default symmetric key encryption algorithm. Note two encryption operations are done when creating an encrypted token: -1) the generated content encryption key is encrypted by the key supplied with the API using the key encryption algorithm such as `RSA-OAEP` -2) the claims are encrypted by the generated content encryption key using the content encryption algorithm such as `A256GCM`. +. The generated content encryption key is encrypted using the supplied key and a key encryption algorithm such as `RSA-OAEP`. +. The claims are encrypted using the content encryption key and a content encryption algorithm such as `A256GCM`. -You can customize the key and content encryption algorithms, for example: +You can customize the key and content encryption algorithms by using the `JwtEncryptionBuilder` API. For example: [source, java] ---- @@ -174,14 +204,15 @@ import io.smallrye.jwt.ContentEncryptionAlgorithm; import io.smallrye.jwt.build.Jwt; // Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. -// Key encryption algorithm is RSA-OAEP-256, content encryption algorithm is A256CBC-HS512. +// Key encryption algorithm is RSA-OAEP-256. The content encryption algorithm is A256CBC-HS512. + String jwt = Jwt.subject("Bob").jwe() .keyAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256) .contentAlgorithm(ContentEncryptionAlgorithm.A256CBC_HS512) .encrypt(); ---- -Alternatively you can use `smallrye.jwt.new-token.key-encryption-algorithm` and `smallrye.jwt.new-token.content-encryption-algorithm` properties to customize the key and content encryption algorithms: +Alternatively, you can configure the algorithms globally by using the following properties: [source,properties] ---- @@ -189,38 +220,42 @@ smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256 smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512 ---- -and write a simpler API sequence: +This configuration allows for a simpler API sequence: [source, java] ---- import io.smallrye.jwt.build.Jwt; -// Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. -// Key encryption algorithm is RSA-OAEP-256, content encryption algorithm is A256CBC-HS512. +// Encrypt the claims by using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. +// Key encryption algorithm is RSA-OAEP-256. The content encryption algorithm is A256CBC-HS512. String jwt = Jwt.subject("Bob").encrypt(); ---- -Note that when the token is directly encrypted by the public RSA or EC key it is not possible to verify which party sent the token. -Therefore, the secret keys should be preferred for directly encrypting the tokens, for example, when using JWT as cookies where a secret key is managed by the Quarkus endpoint with only this endpoint being both a producer and a consumer of the encrypted token. +Recommendations for secure token encryption: -If you would like to use RSA or EC public keys to encrypt the token then it is recommended to sign the token first if the signing key is available, see the next <> section. +- When a token is directly encrypted with a public RSA or EC key, it cannot be verified which party sent the token. +To address this, symmetric secret keys are preferred for direct encryption, especially when using JWT as cookies managed solely by the Quarkus endpoint. +- To encrypt a token with RSA or EC public keys, it is recommended to sign the token first if a signing key is available. For more information, see the <> section. [[innersign-encrypt-claims]] == Sign the claims and encrypt the nested JWT token -The claims can be signed and then the nested JWT token encrypted by combining the sign and encrypt steps. +You can sign the claims and then encrypt the nested JWT token by combining the sign and encrypt steps. + [source, java] ---- import io.smallrye.jwt.build.Jwt; ... -// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations set with the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties respectively. Signature algorithm is RS256, key encryption algorithm is RSA-OAEP-256. +// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations +// specified by the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties, respectively. +// The signature algorithm is RS256, and the key encryption algorithm is RSA-OAEP-256. String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt(); ---- -== Fast JWT Generation +== Fast JWT generation -If `smallrye.jwt.sign.key.location` or/and `smallrye.jwt.encrypt.key.location` properties are set then one can secure the existing claims (resources, maps, JsonObjects) with a single call: +If the `smallrye.jwt.sign.key.location` or `smallrye.jwt.encrypt.key.location` properties are set, you can secure existing claims, such as resources, maps, JsonObjects, with a single call: [source,java] ---- @@ -233,11 +268,12 @@ Jwt.encrypt("/claims.json"); // More compact than Jwt.claims("/claims.json").innerSign().encrypt(); Jwt.signAndEncrypt("/claims.json"); ---- -As mentioned above, `iat` (issued at), `exp` (expires at), `jti` (token identifier), `iss` (issuer) and `aud` (audience) claims will be added if needed. + +As mentioned earlier, the following claims are added automatically if they are not already set: `iat` (issued at), `exp` (expires at), `jti` (token identifier), `iss` (issuer), and `aud` (audience). == Dealing with the keys -`smallrye.jwt.sign.key.location` and `smallrye.jwt.encrypt.key.location` properties can be used to point to signing and encryption key locations. The keys can be located on the local file system, classpath, or fetched from the remote endpoints and can be in `PEM` or `JSON Web Key` (`JWK`) formats. For example: +You can use the `smallrye.jwt.sign.key.location` and `smallrye.jwt.encrypt.key.location` properties to specify the locations of signing and encryption keys. These keys can be located on the local file system, on the classpath, or fetched from remote endpoints. Keys can be in `PEM` or `JSON Web Key (JWK)` formats. For example: [source,properties] ---- @@ -245,7 +281,7 @@ smallrye.jwt.sign.key.location=privateKey.pem smallrye.jwt.encrypt.key.location=publicKey.pem ---- -You can also use MicroProfile `ConfigSource` to fetch the keys from the external services such as link:{vault-guide}[HashiCorp Vault] or other secret managers and use `smallrye.jwt.sign.key` and `smallrye.jwt.encrypt.key` properties instead: +Alternatively, you can fetch keys from external services, such as link:{vault-guide}[HashiCorp Vault] or other secret managers, by using MicroProfile `ConfigSource` and the `smallrye.jwt.sign.key` and `smallrye.jwt.encrypt.key` properties: [source,properties] ---- @@ -253,16 +289,17 @@ smallrye.jwt.sign.key=${private.key.from.vault} smallrye.jwt.encrypt.key=${public.key.from.vault} ---- -where both `private.key.from.vault` and `public.key.from.vault` are the `PEM` or `JWK` formatted key values provided by the custom `ConfigSource`. -`smallrye.jwt.sign.key` and `smallrye.jwt.encrypt.key` can also contain only the Base64-encoded private or public keys values. +In this example, `private.key.from.vault` and `public.key.from.vault` are `PEM` or `JWK` formatted key values provided by the custom `ConfigSource`. + +The `smallrye.jwt.sign.key` and `smallrye.jwt.encrypt.key` properties can also contain Base64-encoded private or public key values directly. -However, please note, directly inlining the private keys in the configuration is not recommended. Use the `smallrye.jwt.sign.key` property only if you need to fetch a signing key value from the remote secret manager. +However, be aware that directly inlining private keys in the configuration is not recommended. Use the `smallrye.jwt.sign.key` property only when you need to fetch a signing key value from a remote secret manager. -The keys can also be loaded by the code which builds the token and supplied to JWT Build API. +The keys can also be loaded by the code that builds the token, and then supplied to JWT Build API for token creation. -If you need to sign and/or encrypt the token using the symmetric secret key then consider using `io.smallrye.jwt.util.KeyUtils` to generate a SecretKey of the required length. +If you need to sign or encrypt the token by using the symmetric secret key, consider using `io.smallrye.jwt.util.KeyUtils` to generate a `SecretKey` of the required length. -For example, one needs to have a 64 byte key to sign using the `HS512` algorithm (`512/8`) and a 32 byte key to encrypt the content encryption key with the `A256KW` algorithm (`256/8`): +For example, a 64-byte key is required to sign a token by using the `HS512` algorithm (`512/8`), and a 32-byte key is needed to encrypt the content encryption key with the `A256KW` algorithm (`256/8`): [source,java] ---- @@ -277,8 +314,9 @@ SecretKey encryptionKey = KeyUtils.generateSecretKey(KeyEncryptionAlgorithm.A256 String jwt = Jwt.claim("sensitiveClaim", getSensitiveClaim()).innerSign(signingKey).encrypt(encryptionKey); ---- -You can also consider using a `JSON Web Key` (JWK) or `JSON Web Key Set` (JWK Set) format to store a secret key on a secure file system and refer to it using either `smallrye.jwt.sign.key.location` or `smallrye.jwt.encrypt.key.location` properties, for example: +You can also consider using a JSON Web Key (JWK) or JSON Web Key Set (JWK Set) format to store a secret key on a secure file system. You can reference the key by using the `smallrye.jwt.sign.key.location` or `smallrye.jwt.encrypt.key.location` properties. +.Example JWK [source,json] ---- { @@ -288,8 +326,7 @@ You can also consider using a `JSON Web Key` (JWK) or `JSON Web Key Set` (JWK Se } ---- -or - +.Example JWK Set [source,json] ---- { @@ -308,36 +345,36 @@ or } ---- -`io.smallrye.jwt.util.KeyUtils` can also be used to generate a pair of asymmetric RSA or EC keys. These keys can be stored using a `JWK`, `JWK Set` or `PEM` format. +You can also use `io.smallrye.jwt.util.KeyUtils` to generate a pair of asymmetric RSA or EC keys. These keys can be stored in `JWK`, `JWK Set`, or `PEM` format. == SmallRye JWT Builder configuration -SmallRye JWT supports the following properties which can be used to customize the way claims are signed and/or encrypted: +SmallRye JWT supports the following properties, which can be used to customize how claims are signed or encrypted: [cols=" Date: Thu, 21 Nov 2024 14:57:57 +0100 Subject: [PATCH 12/38] Qute: if section - adjust the evaluation rules for equality operators - fixes #44610 (cherry picked from commit 9d8b52b7d2957fce055fbed54dcda2ca17962a2f) --- docs/src/main/asciidoc/qute-reference.adoc | 55 +++++++++++++------ .../java/io/quarkus/qute/IfSectionHelper.java | 50 ++++++++++------- .../java/io/quarkus/qute/IfSectionTest.java | 13 +++++ 3 files changed, 81 insertions(+), 37 deletions(-) diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 68a15868301c5..4b62a037bd432 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -774,11 +774,11 @@ A loop section may also define the `{#else}` block that is executed when there a [[if_section]] ==== If Section -The `if` section represents a basic control flow section. +The `{#if}` section represents a basic control flow section. The simplest possible version accepts a single parameter and renders the content if the condition is evaluated to `true`. A condition without an operator evaluates to `true` if the value is not considered `falsy`, i.e. if the value is not `null`, `false`, an empty collection, an empty map, an empty array, an empty string/char sequence or a number equal to zero. -[source] +[source,html] ---- {#if item.active} This item is active. @@ -788,68 +788,91 @@ A condition without an operator evaluates to `true` if the value is not consider You can also use the following operators in a condition: |=== -|Operator |Aliases |Precedence (higher wins) +|Operator |Aliases |Precedence |Example | Description |logical complement |`!` -| 4 +|4 +|`{#if !item.active}{/if}` +|Inverts the evaluated value. |greater than |`gt`, `>` -| 3 +|3 +|`{#if item.age > 43}This item is very old.{/if}` +|Evaluates to `true` if `value1` is greater than `value2`. |greater than or equal to |`ge`, `>=` | 3 +|`{#if item.price >= 100}This item is expensive.{/if}` +|Evaluates to `true` if `value1` is greater than or equal to `value2`. |less than |`lt`, `<` | 3 +|`{#if item.price < 100}This item is cheap.{/if}` +|Evaluates to `true` if `value1` is less than `value2`. |less than or equal to |`le`, `\<=` | 3 +|`{#if item.age <= 43}This item is young.{/if}` +|Evaluates to `true` if `value1` is less than or equal to `value2`. |equals |`eq`, `==`, `is` | 2 +|`{#if item.name eq 'Foo'}Foo item!{/if}` +|Evaluates to `true` if `value1` is equal to `value2`. |not equals |`ne`, `!=` | 2 +|`{#if item.name != 'Bar'}Not a Bar item!{/if}` +|Evaluates to `true` if `value1` is not equal to `value2`. |logical AND (short-circuiting) |`&&`, `and` | 1 +|`{#if item.price > 100 && item.isActive}Expensive and active item.{/if}` +|Evaluates to `true` if both operands evaluate to `true`. |logical OR (short-circuiting) |`\|\|`, `or` | 1 +|`{#if item.price > 100 \|\| item.isActive}Expensive or active item.{/if}` +|Evaluates to `true` if one of the operands evaluates to `true`. |=== -.A simple operator example -[source] ----- -{#if item.age > 10} - This item is very old. -{/if} ----- +For `>`, `>=`, `<`, and `\<=` the following rules are applied: + +* Neither of the operands may be `null`. +* If both operands are of the same type that implements the `java.lang.Comparable` then the `Comparable#compareTo(T)` method is used to perform comparison. +* Otherwise, both operands are coerced to `java.math.BigDecimal` first and then the `BigDecimal#compareTo(BigDecimal)` method is used to perform comparison. + +NOTE: Types that support coercion include `BigInteger`, `Integer`, `Long`, `Double`, `Float` and `String`. + +For `==` and `!=` the following rules are applied: + +* Operands are first tested using the `java.util.Objects#equals(Object, Object)` method. If it returns `true` the operands are considered equal. +* Otherwise, if both operands are not `null` and at least one of them is an instance of `java.lang.Number`, then operands are coerced to `java.math.BigDecimal` and the `BigDecimal#compareTo(BigDecimal)` method is used to perform comparison. Multiple conditions are also supported. .Multiple conditions example -[source] +[source,html] ---- {#if item.age > 10 && item.price > 500} This item is very old and expensive. {/if} ---- -Precedence rules can be overridden by parentheses. +The default precedence rules (higher precedence wins) can be overridden by parentheses. .Parentheses example -[source] +[source,html] ---- {#if (item.age > 10 || item.price > 500) && user.loggedIn} User must be logged in and item age must be > 10 or price must be > 500. @@ -859,7 +882,7 @@ Precedence rules can be overridden by parentheses. You can also add any number of `else` blocks: -[source] +[source,html] ---- {#if item.age > 10} This item is very old. diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java index 48e2ae24dee02..59e5197da6108 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java @@ -476,9 +476,9 @@ int getPrecedence() { boolean evaluate(Object op1, Object op2) { switch (this) { case EQ: - return Objects.equals(op1, op2); + return equals(op1, op2); case NE: - return !Objects.equals(op1, op2); + return !equals(op1, op2); case GE: case GT: case LE: @@ -492,6 +492,17 @@ boolean evaluate(Object op1, Object op2) { } } + boolean equals(Object op1, Object op2) { + if (Objects.equals(op1, op2)) { + return true; + } + if (op1 != null && op2 != null && (op1 instanceof Number || op2 instanceof Number)) { + // Both operands are not null and at least one of them is a number + return getDecimal(op1).compareTo(getDecimal(op2)) == 0; + } + return false; + } + @SuppressWarnings({ "rawtypes", "unchecked" }) boolean compare(Object op1, Object op2) { if (op1 == null || op2 == null) { @@ -553,25 +564,22 @@ static Operator from(String value) { } static BigDecimal getDecimal(Object value) { - BigDecimal decimal; - if (value instanceof BigDecimal) { - decimal = (BigDecimal) value; - } else if (value instanceof BigInteger) { - decimal = new BigDecimal((BigInteger) value); - } else if (value instanceof Integer) { - decimal = new BigDecimal((Integer) value); - } else if (value instanceof Long) { - decimal = new BigDecimal((Long) value); - } else if (value instanceof Double) { - decimal = new BigDecimal((Double) value); - } else if (value instanceof Float) { - decimal = new BigDecimal((Float) value); - } else if (value instanceof String) { - decimal = new BigDecimal(value.toString()); - } else { - throw new TemplateException("Not a valid number: " + value); - } - return decimal; + if (value instanceof BigDecimal decimal) { + return decimal; + } else if (value instanceof BigInteger bigInteger) { + return new BigDecimal(bigInteger); + } else if (value instanceof Integer integer) { + return BigDecimal.valueOf(integer); + } else if (value instanceof Long _long) { + return BigDecimal.valueOf(_long); + } else if (value instanceof Double _double) { + return BigDecimal.valueOf(_double); + } else if (value instanceof Float _float) { + return BigDecimal.valueOf(_float); + } else if (value instanceof String string) { + return new BigDecimal(string); + } + throw new TemplateException("Cannot coerce " + value + " to a BigDecimal"); } } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java index 3f0d88c39f2a4..d260a6c608a5e 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java @@ -262,6 +262,19 @@ public void testParameterOrigin() { } } + @Test + public void testComparisons() { + Engine engine = Engine.builder().addDefaults().build(); + assertEquals("longGtInt", engine.parse("{#if val > 10}longGtInt{/if}").data("val", 11l).render()); + assertEquals("doubleGtInt", engine.parse("{#if val > 10}doubleGtInt{/if}").data("val", 20.0).render()); + assertEquals("longGtStr", engine.parse("{#if val > '10'}longGtStr{/if}").data("val", 11l).render()); + assertEquals("longLeStr", engine.parse("{#if val <= '10'}longLeStr{/if}").data("val", 1l).render()); + assertEquals("longEqInt", engine.parse("{#if val == 10}longEqInt{/if}").data("val", 10l).render()); + assertEquals("doubleEqInt", engine.parse("{#if val == 10}doubleEqInt{/if}").data("val", 10.0).render()); + assertEquals("doubleEqFloat", engine.parse("{#if val == 10.00f}doubleEqFloat{/if}").data("val", 10.0).render()); + assertEquals("longEqLong", engine.parse("{#if val eq 10l}longEqLong{/if}").data("val", Long.valueOf(10)).render()); + } + public static class Target { public ContentStatus status; From 79d7ed057bc272c0c356e4afd838311bfe3c40ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 8 Nov 2024 09:56:17 +0100 Subject: [PATCH 13/38] Ignore some documentation-related annotations in Hibernate ORM extension tests (cherry picked from commit 2478d65e1b67eaa7f0be267c930a583d068c922c) --- .../io/quarkus/hibernate/orm/deployment/ClassNamesTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/deployment/ClassNamesTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/deployment/ClassNamesTest.java index 60b94f4c7ddef..66ca64943ca52 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/deployment/ClassNamesTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/deployment/ClassNamesTest.java @@ -107,6 +107,8 @@ public void testNoMissingHibernateAnnotation() { } private static void ignoreInternalAnnotations(Set annotationSet) { + annotationSet.removeIf(name -> name.toString().equals("org.hibernate.cfg.Compatibility")); + annotationSet.removeIf(name -> name.toString().equals("org.hibernate.cfg.Unsafe")); annotationSet.removeIf(name -> name.toString().equals("org.hibernate.Incubating")); annotationSet.removeIf(name -> name.toString().equals("org.hibernate.Internal")); annotationSet.removeIf(name -> name.toString().equals("org.hibernate.Remove")); From 672abaf506ea48c8b4227cd6de8674669a546c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 8 Nov 2024 09:56:36 +0100 Subject: [PATCH 14/38] Upgrade to Hibernate ORM 6.6.2.Final (cherry picked from commit e6dfe7a3539378e029cef5ccdcdac12208da087d) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 94c31a88da386..99ad29a97941e 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 0.8.12 6.13.4 5.5.0 - 6.6.1.Final + 6.6.2.Final 4.13.0 1.14.18 7.0.3.Final From a705a7556338034d0744779c1b18767c47320bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 8 Nov 2024 16:15:27 +0100 Subject: [PATCH 15/38] Upgrade to Hibernate ORM 6.6.3.Final (cherry picked from commit 3ec3c5f6fdc4e63c615aaefb2f05902c66b80bbf) --- .../quarkus/hibernate/orm/deployment/ClassNames.java | 3 +++ .../hibernate/orm/deployment/GraalVMFeatures.java | 10 ++++++++++ pom.xml | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java index 3f79f8d71ba34..f7e6bd6d5f620 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java @@ -455,6 +455,9 @@ private static DotName createConstant(String fqcn) { createConstant("java.util.UUID"), createConstant("java.lang.Void")); + public static final List STANDARD_STACK_ELEMENT_TYPES = List.of( + createConstant("org.hibernate.query.sqm.tree.select.SqmQueryPart")); + public static final DotName HIBERNATE_ORM_PROCESSOR = createConstant( "io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor"); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java index 652de267cc10f..d964cdae8694c 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java @@ -39,4 +39,14 @@ ReflectiveClassBuildItem registerJdbcArrayTypesForReflection() { .build(); } + // Workaround for https://hibernate.atlassian.net/browse/HHH-18875 + // See https://hibernate.zulipchat.com/#narrow/channel/132094-hibernate-orm-dev/topic/StandardStack.20and.20reflection + @BuildStep + ReflectiveClassBuildItem registerStandardStackElementTypesForReflection() { + return ReflectiveClassBuildItem + .builder(ClassNames.STANDARD_STACK_ELEMENT_TYPES.stream().map(d -> d.toString() + "[]").toArray(String[]::new)) + .reason("Workaround for https://hibernate.atlassian.net/browse/HHH-18875") + .build(); + } + } diff --git a/pom.xml b/pom.xml index 99ad29a97941e..f2f38e6ee861d 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 0.8.12 6.13.4 5.5.0 - 6.6.2.Final + 6.6.3.Final 4.13.0 1.14.18 7.0.3.Final From c0ec3dd61915ed657723e59f7fc29e27c12941ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 8 Nov 2024 17:34:04 +0100 Subject: [PATCH 16/38] Fail the build instead of skipping Hibernate ORM bytecode enhancement when it's unsupported (cherry picked from commit b827df5e5f2d96359fe46617395544cdebbd413f) --- .../integration/QuarkusEnhancementContext.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/integration/QuarkusEnhancementContext.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/integration/QuarkusEnhancementContext.java index bdbbe3ee916b3..3afc9e59e5c5b 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/integration/QuarkusEnhancementContext.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/integration/QuarkusEnhancementContext.java @@ -2,6 +2,7 @@ import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; public final class QuarkusEnhancementContext extends DefaultEnhancementContext { @@ -26,4 +27,15 @@ public ClassLoader getLoadingClassLoader() { throw new IllegalStateException("The Classloader of the EnhancementContext should not be used"); } + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + // We expect model classes to be enhanced. + // Lack of enhancement could lead to many problems, + // from bad performance, to Quarkus-specific optimizations causing errors/data loss, + // to incorrect generated bytecode (references to non-existing methods). + // If something prevents enhancement, it's just safer to have Hibernate ORM's enhancer fail + // with a clear error message pointing to the application class that needs to be fixed. + return UnsupportedEnhancementStrategy.FAIL; + } + } From 99a3f42c189303c3c969cd4210b2fe757e41e2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 23 Nov 2024 15:13:13 +0100 Subject: [PATCH 17/38] fix(security,kotlin) Support @AuthorizationPolicy on suspended endpoints (cherry picked from commit 61cdd8851659792e24a0bdda8a913af271d82b3f) --- .../deployment/HttpSecurityProcessor.java | 10 +++- .../resteasy-reactive-kotlin/standard/pom.xml | 17 +++++++ .../reactive/kotlin/SecuredClassResource.kt | 17 +++++++ .../reactive/kotlin/SecuredMethodResource.kt | 14 ++++++ .../kotlin/SuspendAuthorizationPolicy.kt | 26 ++++++++++ .../resteasy/reactive/kotlin/SecurityTest.kt | 47 +++++++++++++++++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 80a2f6f2ee832..51829fb5db14e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -95,6 +95,7 @@ public class HttpSecurityProcessor { private static final DotName AUTH_MECHANISM_NAME = DotName.createSimple(HttpAuthenticationMechanism.class); private static final DotName BASIC_AUTH_MECH_NAME = DotName.createSimple(BasicAuthenticationMechanism.class); private static final DotName BASIC_AUTH_ANNOTATION_NAME = DotName.createSimple(BasicAuthentication.class); + private static final String KOTLIN_SUSPEND_IMPL_SUFFIX = "$suspendImpl"; @Record(ExecutionTime.STATIC_INIT) @BuildStep @@ -576,9 +577,16 @@ private static Stream getPolicyTargetEndpointCandidates(AnnotationTa if (target.kind() == AnnotationTarget.Kind.METHOD) { var method = target.asMethod(); if (!hasProperEndpointModifiers(method)) { + if (method.isSynthetic() && method.name().endsWith(KOTLIN_SUSPEND_IMPL_SUFFIX)) { + // ATM there are 2 methods for Kotlin endpoint like this: + // @AuthorizationPolicy(name = "suspended") + // suspend fun sayHi() = "Hi" + // the synthetic method doesn't need to be secured, but it keeps security annotations + return Stream.empty(); + } throw new RuntimeException(""" Found method annotated with the @AuthorizationPolicy annotation that is not an endpoint: %s#%s - """.formatted(method.asClass().name().toString(), method.name())); + """.formatted(method.declaringClass().name().toString(), method.name())); } return Stream.of(method); } diff --git a/integration-tests/resteasy-reactive-kotlin/standard/pom.xml b/integration-tests/resteasy-reactive-kotlin/standard/pom.xml index b20e5a1f5192f..d37324da463ac 100644 --- a/integration-tests/resteasy-reactive-kotlin/standard/pom.xml +++ b/integration-tests/resteasy-reactive-kotlin/standard/pom.xml @@ -39,6 +39,10 @@ io.quarkus quarkus-kotlin + + io.quarkus + quarkus-security + io.quarkus quarkus-integration-test-shared-library @@ -186,6 +190,19 @@ + + io.quarkus + quarkus-security-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt new file mode 100644 index 0000000000000..214bfc4cf8cd1 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt @@ -0,0 +1,17 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.vertx.http.security.AuthorizationPolicy +import jakarta.annotation.security.PermitAll +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +@AuthorizationPolicy(name = "suspended") +@Path("/secured-class") +class SecuredClassResource { + + @Path("/authorization-policy-suspend") + @GET + suspend fun authorizationPolicySuspend() = "Hello from Quarkus REST" + + @PermitAll @Path("/public") @GET suspend fun publicEndpoint() = "Hello to everyone!" +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt new file mode 100644 index 0000000000000..f2d9134894dce --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt @@ -0,0 +1,14 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.vertx.http.security.AuthorizationPolicy +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +@Path("/secured-method") +class SecuredMethodResource { + + @Path("/authorization-policy-suspend") + @GET + @AuthorizationPolicy(name = "suspended") + suspend fun authorizationPolicySuspend() = "Hello from Quarkus REST" +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt new file mode 100644 index 0000000000000..9d54e64bed799 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt @@ -0,0 +1,26 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.security.identity.SecurityIdentity +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult +import io.smallrye.mutiny.Uni +import io.vertx.core.http.HttpHeaders +import io.vertx.ext.web.RoutingContext +import jakarta.enterprise.context.ApplicationScoped + +@ApplicationScoped +class SuspendAuthorizationPolicy : HttpSecurityPolicy { + override fun checkPermission( + request: RoutingContext?, + identity: Uni?, + requestContext: HttpSecurityPolicy.AuthorizationRequestContext? + ): Uni { + val authZHeader = request?.request()?.getHeader(HttpHeaders.AUTHORIZATION) + if (authZHeader == "you-can-trust-me") { + return CheckResult.permit() + } + return CheckResult.deny() + } + + override fun name() = "suspended" +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt new file mode 100644 index 0000000000000..a8b81532d5dbe --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt @@ -0,0 +1,47 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import io.vertx.core.http.HttpHeaders +import org.hamcrest.CoreMatchers +import org.junit.jupiter.api.Test + +@QuarkusTest +class SecurityTest { + + @Test + fun testAuthorizationPolicyOnSuspendedMethod_MethodLevel() { + When { get("/secured-method/authorization-policy-suspend") } Then { statusCode(403) } + Given { header(HttpHeaders.AUTHORIZATION.toString(), "you-can-trust-me") } When + { + get("/secured-method/authorization-policy-suspend") + } Then + { + statusCode(200) + body(CoreMatchers.`is`("Hello from Quarkus REST")) + } + } + + @Test + fun testAuthorizationPolicyOnSuspendedMethod_ClassLevel() { + // test class-level annotation is applied on a secured method + When { get("/secured-class/authorization-policy-suspend") } Then { statusCode(403) } + Given { header(HttpHeaders.AUTHORIZATION.toString(), "you-can-trust-me") } When + { + get("/secured-class/authorization-policy-suspend") + } Then + { + statusCode(200) + body(CoreMatchers.`is`("Hello from Quarkus REST")) + } + + // test method-level @PermitAll has priority over @AuthorizationPolicy on the class + When { get("/secured-class/public") } Then + { + statusCode(200) + body(CoreMatchers.`is`("Hello to everyone!")) + } + } +} From 89179ff62ec69e4893db48ad31d9d026ad223ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 24 Nov 2024 12:08:31 +0100 Subject: [PATCH 18/38] fix(rest-jackson,security): improve @SecureField annotation detection (cherry picked from commit e8fd90aaea6818ac528295bbddf4c429500200fe) --- docs/src/main/asciidoc/rest.adoc | 3 + .../ResteasyReactiveJacksonProcessor.java | 50 ++++++++-- .../test/DisableSecureSerializationTest.java | 94 +++++++++++++++++++ .../jackson/deployment/test/Fruit.java | 16 ++++ .../deployment/test/GenericWrapper.java | 14 +++ .../jackson/deployment/test/Price.java | 17 ++++ .../deployment/test/SimpleJsonResource.java | 34 +++---- .../deployment/test/SimpleJsonTest.java | 23 ++++- ...JsonWithReflectionFreeSerializersTest.java | 12 ++- 9 files changed, 230 insertions(+), 33 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/DisableSecureSerializationTest.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Fruit.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/GenericWrapper.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Price.java diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc index aa0ffb3752396..c3ffbf337e6d9 100644 --- a/docs/src/main/asciidoc/rest.adoc +++ b/docs/src/main/asciidoc/rest.adoc @@ -1535,6 +1535,9 @@ WARNING: Currently you cannot use the `@SecureField` annotation to secure your d ==== All resource methods returning data secured with the `@SecureField` annotation should be tested. Please make sure data are secured as you intended. +Quarkus always attempts to detect fields annotated with the `@SecureField` annotation, +however it may fail to infer returned type and miss the `@SecureField` annotation instance. +If that happens, please explicitly enable secure serialization on the resource endpoint with the `@EnableSecureSerialization` annotation. ==== Assuming security has been set up for the application (see our xref:security-overview.adoc[guide] for more details), when a user with the `admin` role diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index 43ef5caad8bea..b8607985a1bb8 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -455,6 +455,8 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r continue; } } + boolean secureSerializationExplicitlyEnabled = methodInfo.hasAnnotation(ENABLE_SECURE_SERIALIZATION) + || entry.getActualClassInfo().hasDeclaredAnnotation(ENABLE_SECURE_SERIALIZATION); ResourceMethod resourceInfo = entry.getResourceMethod(); boolean isJsonResponse = false; @@ -470,12 +472,31 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r continue; } - ClassInfo effectiveReturnClassInfo = getEffectiveClassInfo(methodInfo.returnType(), indexView); + var methodReturnType = methodInfo.returnType(); + ClassInfo effectiveReturnClassInfo = getEffectiveClassInfo(methodReturnType, indexView); if (effectiveReturnClassInfo == null) { continue; } + + final Map typeParamIdentifierToParameterizedType; + if (methodReturnType.kind() == Type.Kind.PARAMETERIZED_TYPE) { + typeParamIdentifierToParameterizedType = new HashMap<>(); + var parametrizedReturnType = methodReturnType.asParameterizedType(); + for (int i = 0; i < parametrizedReturnType.arguments().size(); i++) { + if (i < effectiveReturnClassInfo.typeParameters().size()) { + var identifier = effectiveReturnClassInfo.typeParameters().get(i).identifier(); + var parametrizedTypeArg = parametrizedReturnType.arguments().get(i); + typeParamIdentifierToParameterizedType.put(identifier, parametrizedTypeArg); + } + } + } else { + typeParamIdentifierToParameterizedType = null; + } + AtomicBoolean needToDeleteCache = new AtomicBoolean(false); - if (hasSecureFields(indexView, effectiveReturnClassInfo, typeToHasSecureField, needToDeleteCache)) { + if (secureSerializationExplicitlyEnabled + || hasSecureFields(indexView, effectiveReturnClassInfo, typeToHasSecureField, needToDeleteCache, + typeParamIdentifierToParameterizedType)) { AnnotationInstance customSerializationAtClassAnnotation = methodInfo.declaringClass() .declaredAnnotation(CUSTOM_SERIALIZATION); AnnotationInstance customSerializationAtMethodAnnotation = methodInfo.annotation(CUSTOM_SERIALIZATION); @@ -539,7 +560,8 @@ private static Map getTypesWithSecureField() { } private static boolean hasSecureFields(IndexView indexView, ClassInfo currentClassInfo, - Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + Map typeToHasSecureField, AtomicBoolean needToDeleteCache, + Map typeParamIdentifierToParameterizedType) { // use cached result if there is any final String className = currentClassInfo.name().toString(); if (typeToHasSecureField.containsKey(className)) { @@ -565,7 +587,7 @@ private static boolean hasSecureFields(IndexView indexView, ClassInfo currentCla } else { // check interface implementors as anyone of them can be returned hasSecureFields = indexView.getAllKnownImplementors(currentClassInfo.name()).stream() - .anyMatch(ci -> hasSecureFields(indexView, ci, typeToHasSecureField, needToDeleteCache)); + .anyMatch(ci -> hasSecureFields(indexView, ci, typeToHasSecureField, needToDeleteCache, null)); } } else { // figure if any field or parent / subclass field is secured @@ -576,7 +598,7 @@ private static boolean hasSecureFields(IndexView indexView, ClassInfo currentCla hasSecureFields = false; } else { hasSecureFields = anyFieldHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, - needToDeleteCache) + needToDeleteCache, typeParamIdentifierToParameterizedType) || anySubclassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache) || anyParentClassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache); @@ -600,7 +622,7 @@ private static boolean anyParentClassHasSecureFields(IndexView indexView, ClassI if (!currentClassInfo.superName().equals(ResteasyReactiveDotNames.OBJECT)) { final ClassInfo parentClassInfo = indexView.getClassByName(currentClassInfo.superName()); return parentClassInfo != null - && hasSecureFields(indexView, parentClassInfo, typeToHasSecureField, needToDeleteCache); + && hasSecureFields(indexView, parentClassInfo, typeToHasSecureField, needToDeleteCache, null); } return false; } @@ -608,16 +630,26 @@ private static boolean anyParentClassHasSecureFields(IndexView indexView, ClassI private static boolean anySubclassHasSecureFields(IndexView indexView, ClassInfo currentClassInfo, Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { return indexView.getAllKnownSubclasses(currentClassInfo.name()).stream() - .anyMatch(subclass -> hasSecureFields(indexView, subclass, typeToHasSecureField, needToDeleteCache)); + .anyMatch(subclass -> hasSecureFields(indexView, subclass, typeToHasSecureField, needToDeleteCache, null)); } private static boolean anyFieldHasSecureFields(IndexView indexView, ClassInfo currentClassInfo, - Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + Map typeToHasSecureField, AtomicBoolean needToDeleteCache, + Map typeParamIdentifierToParameterizedType) { return currentClassInfo .fields() .stream() .filter(fieldInfo -> !fieldInfo.hasAnnotation(JSON_IGNORE)) .map(FieldInfo::type) + .map(fieldType -> { + if (typeParamIdentifierToParameterizedType != null && fieldType.kind() == Type.Kind.TYPE_VARIABLE) { + var typeVariable = typeParamIdentifierToParameterizedType.get(fieldType.asTypeVariable().identifier()); + if (typeVariable != null) { + return typeVariable; + } + } + return fieldType; + }) .anyMatch(fieldType -> fieldTypeHasSecureFields(fieldType, indexView, typeToHasSecureField, needToDeleteCache)); } @@ -629,7 +661,7 @@ private static boolean fieldTypeHasSecureFields(Type fieldType, IndexView indexV return false; } final ClassInfo fieldClass = indexView.getClassByName(fieldType.name()); - return fieldClass != null && hasSecureFields(indexView, fieldClass, typeToHasSecureField, needToDeleteCache); + return fieldClass != null && hasSecureFields(indexView, fieldClass, typeToHasSecureField, needToDeleteCache, null); } if (fieldType.kind() == Type.Kind.ARRAY) { return fieldTypeHasSecureFields(fieldType.asArrayType().constituent(), indexView, typeToHasSecureField, diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/DisableSecureSerializationTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/DisableSecureSerializationTest.java new file mode 100644 index 0000000000000..e033c46cd2b1b --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/DisableSecureSerializationTest.java @@ -0,0 +1,94 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization; +import io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization; +import io.quarkus.resteasy.reactive.jackson.SecureField; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.response.ValidatableResponse; + +public class DisableSecureSerializationTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class)); + + @Test + public void testDisablingOfSecureSerialization() { + request("disabled", "user").body("secretField", Matchers.is("secret")); + request("disabled", "admin").body("secretField", Matchers.is("secret")); + request("enabled", "user").body("secretField", Matchers.nullValue()); + request("enabled", "admin").body("secretField", Matchers.is("secret")); + } + + private static ValidatableResponse request(String subPath, String user) { + TestIdentityController.resetRoles().add(user, user, user); + return RestAssured + .with() + .auth().preemptive().basic(user, user) + .get("/test/" + subPath) + .then() + .statusCode(200) + .body("publicField", Matchers.is("public")); + } + + @DisableSecureSerialization + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Path("test") + public static class GreetingsResource { + + @Path("disabled") + @GET + public Dto disabled() { + return Dto.createDto(); + } + + @EnableSecureSerialization + @Path("enabled") + @GET + public Dto enabled() { + return Dto.createDto(); + } + } + + public static class Dto { + + public Dto(String secretField, String publicField) { + this.secretField = secretField; + this.publicField = publicField; + } + + @SecureField(rolesAllowed = "admin") + private final String secretField; + + private final String publicField; + + public String getSecretField() { + return secretField; + } + + public String getPublicField() { + return publicField; + } + + private static Dto createDto() { + return new Dto("secret", "public"); + } + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Fruit.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Fruit.java new file mode 100644 index 0000000000000..32b9d539de12c --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Fruit.java @@ -0,0 +1,16 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.util.List; + +public class Fruit { + + public String name; + + public List prices; + + public Fruit(String name, Float price) { + this.name = name; + this.prices = List.of(new Price("USD", price)); + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/GenericWrapper.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/GenericWrapper.java new file mode 100644 index 0000000000000..c661128e6220c --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/GenericWrapper.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +public class GenericWrapper { + + public String name; + + public T entity; + + public GenericWrapper(String name, T entity) { + this.name = name; + this.entity = entity; + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Price.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Price.java new file mode 100644 index 0000000000000..f457ef415fd21 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Price.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import io.quarkus.resteasy.reactive.jackson.SecureField; + +public class Price { + + @SecureField(rolesAllowed = "admin") + public Float price; + + public String currency; + + public Price(String currency, Float price) { + this.currency = currency; + this.price = price; + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 861f01ce08a96..2b79bbadada79 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -33,7 +33,6 @@ import io.quarkus.resteasy.reactive.jackson.CustomDeserialization; import io.quarkus.resteasy.reactive.jackson.CustomSerialization; import io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization; -import io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization; import io.quarkus.runtime.BlockingOperationControl; import io.smallrye.common.annotation.NonBlocking; import io.smallrye.mutiny.Multi; @@ -41,7 +40,6 @@ @Path("/simple") @NonBlocking -@DisableSecureSerialization public class SimpleJsonResource extends SuperClass { @ServerExceptionMapper @@ -50,6 +48,7 @@ public Response handleParseException(WebApplicationException e) { return Response.status(Response.Status.BAD_REQUEST).entity(cause.getMessage()).build(); } + @DisableSecureSerialization @GET @Path("/person") public Person getPerson() { @@ -61,7 +60,6 @@ public Person getPerson() { return person; } - @EnableSecureSerialization @GET @Path("/frog") public Frog getFrog() { @@ -74,35 +72,30 @@ public Frog getFrog() { return frog; } - @EnableSecureSerialization @GET @Path("/frog-body-parts") public FrogBodyParts getFrogBodyParts() { return new FrogBodyParts("protruding eyes"); } - @EnableSecureSerialization @GET @Path("/interface-dog") public SecuredPersonInterface getInterfaceDog() { return createDog(); } - @EnableSecureSerialization @GET @Path("/abstract-dog") public AbstractPet getAbstractDog() { return createDog(); } - @EnableSecureSerialization @GET @Path("/abstract-named-dog") public AbstractNamedPet getAbstractNamedDog() { return createDog(); } - @EnableSecureSerialization @GET @Path("/dog") public Dog getDog() { @@ -130,48 +123,48 @@ public MapWrapper echoNullMap(MapWrapper mapWrapper) { return mapWrapper; } - @EnableSecureSerialization @GET @Path("/abstract-cat") public AbstractPet getAbstractCat() { return createCat(); } - @EnableSecureSerialization @GET @Path("/interface-cat") public SecuredPersonInterface getInterfaceCat() { return createCat(); } - @EnableSecureSerialization @GET @Path("/abstract-named-cat") public AbstractNamedPet getAbstractNamedCat() { return createCat(); } - @EnableSecureSerialization @GET @Path("/cat") public Cat getCat() { return createCat(); } - @EnableSecureSerialization @GET @Path("/unsecured-pet") public UnsecuredPet getUnsecuredPet() { return createUnsecuredPet(); } - @EnableSecureSerialization @GET @Path("/abstract-unsecured-pet") public AbstractUnsecuredPet getAbstractUnsecuredPet() { return createUnsecuredPet(); } + @GET + @Path("/secure-field-on-type-variable") + public GenericWrapper getWithSecureFieldOnTypeVariable() { + return new GenericWrapper<>("wrapper", new Fruit("Apple", 1.0f)); + } + private static UnsecuredPet createUnsecuredPet() { var pet = new UnsecuredPet(); pet.setPublicName("Unknown"); @@ -212,6 +205,7 @@ public Person getCustomSerializedPerson() { return getPerson(); } + @DisableSecureSerialization @CustomDeserialization(UnquotedFieldsPersonDeserialization.class) @POST @Path("custom-deserialized-person") @@ -219,7 +213,6 @@ public Person echoCustomDeserializedPerson(Person request) { return request; } - @EnableSecureSerialization @GET @Path("secure-person") public Person getSecurePerson() { @@ -227,7 +220,6 @@ public Person getSecurePerson() { } @JsonView(Views.Public.class) - @EnableSecureSerialization @GET @Path("secure-person-with-public-view") public Person getSecurePersonWithPublicView() { @@ -235,7 +227,6 @@ public Person getSecurePersonWithPublicView() { } @JsonView(Views.Public.class) - @EnableSecureSerialization @GET @Path("uni-secure-person-with-public-view") public Uni getUniSecurePersonWithPublicView() { @@ -243,41 +234,37 @@ public Uni getUniSecurePersonWithPublicView() { } @JsonView(Views.Private.class) - @EnableSecureSerialization @GET @Path("secure-person-with-private-view") public Person getSecurePersonWithPrivateView() { return getPerson(); } - @EnableSecureSerialization @GET @Path("secure-uni-person") public Uni getSecureUniPerson() { return Uni.createFrom().item(getPerson()); } - @EnableSecureSerialization @GET @Path("secure-rest-response-person") public RestResponse getSecureRestResponsePerson() { return RestResponse.ok(getPerson()); } - @EnableSecureSerialization @GET @Path("secure-people") public List getSecurePeople() { return Collections.singletonList(getPerson()); } - @EnableSecureSerialization @GET @Path("secure-uni-people") public Uni> getSecureUniPeople() { return Uni.createFrom().item(Collections.singletonList(getPerson())); } + @DisableSecureSerialization @POST @Path("/person") @Produces(MediaType.APPLICATION_JSON) @@ -322,6 +309,7 @@ public Response getPersonCustomMediaTypeResponseWithType(Person person) { return Response.ok(person).status(201).header("Content-Type", "application/vnd.quarkus.other-v1+json").build(); } + @DisableSecureSerialization @POST @Path("/people") @Consumes(MediaType.APPLICATION_JSON) @@ -354,6 +342,7 @@ public List strings(List strings) { return strings; } + @DisableSecureSerialization @POST @Path("/person-large") @Produces(MediaType.APPLICATION_JSON) @@ -365,6 +354,7 @@ public Person personTest(Person person) { return person; } + @DisableSecureSerialization @POST @Path("/person-validated") @Produces(MediaType.APPLICATION_JSON) diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index a5fa4d498c923..4d1092f546893 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -36,7 +36,8 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class, MapWrapper.class) + NestedInterface.class, StateRecord.class, MapWrapper.class, GenericWrapper.class, + Fruit.class, Price.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n"), "application.properties"); @@ -584,6 +585,26 @@ public void testSecureFieldOnArrayTypeField() { .body("parts[0].name", Matchers.is("protruding eyes")); } + @Test + public void testSecureFieldOnTypeVariable() { + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured + .with() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-field-on-type-variable") + .then() + .statusCode(200) + .body("entity.prices[0].price", Matchers.nullValue()); + TestIdentityController.resetRoles().add("rolfe", "rolfe", "admin"); + RestAssured + .with() + .auth().preemptive().basic("rolfe", "rolfe") + .get("/simple/secure-field-on-type-variable") + .then() + .statusCode(200) + .body("entity.prices[0].price", Matchers.notNullValue()); + } + private static void testSecuredFieldOnReturnTypeField(String subPath) { TestIdentityController.resetRoles().add("max", "max", "user"); RestAssured diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java index 10ea3d373ce91..d0fdd70214f1b 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java @@ -5,6 +5,8 @@ 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.Disabled; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.security.test.utils.TestIdentityController; @@ -25,7 +27,8 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class, MapWrapper.class) + NestedInterface.class, StateRecord.class, MapWrapper.class, GenericWrapper.class, + Fruit.class, Price.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n" + @@ -33,4 +36,11 @@ public JavaArchive get() { "application.properties"); } }); + + @Disabled("Doesn't work with the reflection free serializers") + @Test + @Override + public void testSecureFieldOnTypeVariable() { + super.testSecureFieldOnTypeVariable(); + } } From 9b6ac3f8c9d70d5aee19dae9855dd1d8b63eec1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 23 Nov 2024 13:06:38 +0100 Subject: [PATCH 19/38] fix(spring-security): Pass @PreAuthorize method args. to SpringWeb endpoints (cherry picked from commit 245a28181ee69641327d89d3de76b74d22332e5f) --- .../deployment/BeanMethodInvocationGenerator.java | 9 +++++++++ .../io/quarkus/it/spring/security/PersonChecker.java | 10 ++++++++++ .../io/quarkus/it/spring/security/TheController.java | 6 ++++++ .../java/io/quarkus/it/spring/web/SecurityTest.java | 10 ++++++++++ 4 files changed, 35 insertions(+) create mode 100644 integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/PersonChecker.java diff --git a/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/BeanMethodInvocationGenerator.java b/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/BeanMethodInvocationGenerator.java index 8481e12027f93..16bf7896d1b30 100644 --- a/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/BeanMethodInvocationGenerator.java +++ b/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/BeanMethodInvocationGenerator.java @@ -130,6 +130,7 @@ final String generateSecurityCheck(String expression, MethodInfo securedMethodIn instanceNullTrue.returnValue(newInstance); } + boolean checkRequiresMethodArguments = false; try (MethodCreator check = cc.getMethodCreator("check", boolean.class, SecurityIdentity.class, Object[].class) .setModifiers(Modifier.PROTECTED)) { ResultHandle arcContainer = check @@ -155,6 +156,7 @@ final String generateSecurityCheck(String expression, MethodInfo securedMethodIn argHandles[i] = check.load(argumentExpression.replace("'", "")); } else if (trimmedArgumentExpression.matches(METHOD_PARAMETER_REGEX)) { // secured method's parameter case + checkRequiresMethodArguments = true; Matcher parameterMatcher = METHOD_PARAMETER_PATTERN.matcher(trimmedArgumentExpression); if (!parameterMatcher.find()) { // should never happen throw createGenericMalformedException(securedMethodInfo, expression); @@ -206,6 +208,13 @@ final String generateSecurityCheck(String expression, MethodInfo securedMethodIn check.returnValue(result); } + + if (checkRequiresMethodArguments) { + try (MethodCreator check = cc.getMethodCreator("requiresMethodArguments", boolean.class) + .setModifiers(Modifier.PUBLIC)) { + check.returnBoolean(true); + } + } } beansReferencedInPreAuthorized.add(beanClassInfo.name().toString()); diff --git a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/PersonChecker.java b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/PersonChecker.java new file mode 100644 index 0000000000000..2a24ee0dfae1d --- /dev/null +++ b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/PersonChecker.java @@ -0,0 +1,10 @@ +package io.quarkus.it.spring.security; + +import org.springframework.stereotype.Component; + +@Component +public class PersonChecker { + public boolean check(String name) { + return name.equals("correct-name"); + } +} diff --git a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/TheController.java b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/TheController.java index 118ec627e1e52..bab3b616989e1 100644 --- a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/TheController.java +++ b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/security/TheController.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -63,4 +64,9 @@ public String preAuthorizeOnController() { return "preAuthorizeOnController"; } + @GetMapping("/preAuthorizeOnControllerWithArgs/{user}") + @PreAuthorize("@personChecker.check(#user)") + public String preAuthorizeOnControllerWithArgs(@PathVariable String user) { + return "Hello " + user + "!"; + } } diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java index 02452e84e90e9..82a6ea35aea9f 100644 --- a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java @@ -50,6 +50,16 @@ public void testPreAuthorizeOnController() { Optional.of("preAuthorizeOnController")); } + @Test + public void preAuthorizeOnControllerWithArgs() { + String path = "/api/preAuthorizeOnControllerWithArgs/"; + assertForAnonymous(path + "correct-name", 401, Optional.empty()); + assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path + "wrong-name", 403, + Optional.empty()); + assertStatusAndContent(RestAssured.given().auth().preemptive().basic("aurea", "auri"), path + "correct-name", 200, + Optional.of("Hello correct-name!")); + } + @Test public void shouldAccessAllowed() { assertForAnonymous("/api/accessibleForAllMethod", 200, Optional.of("accessibleForAll")); From 700c0d176930b2071d74dff71a796e69e89a3ad3 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 22 Nov 2024 10:38:12 +0200 Subject: [PATCH 20/38] Clean up effects of test profiles when @QuarkusMainTest completes Fixes: #44117 (cherry picked from commit 71804ebd709e39c11eb35116e68b8fca8aefe41d) --- .../test/junit/QuarkusMainTestExtension.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java index 107c2f9b60b81..7975bd7b1b18d 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java @@ -43,6 +43,7 @@ public class QuarkusMainTestExtension extends AbstractJvmQuarkusTestExtension AfterAllCallback, ExecutionCondition { PrepareResult prepareResult; + LinkedBlockingDeque shutdownTasks; /** * The result from an {@link Launch} test @@ -79,9 +80,8 @@ private void ensurePrepared(ExtensionContext extensionContext, Class shutdownTasks = new LinkedBlockingDeque<>(); - PrepareResult result = createAugmentor(extensionContext, profile, shutdownTasks); - prepareResult = result; + shutdownTasks = new LinkedBlockingDeque<>(); + prepareResult = createAugmentor(extensionContext, profile, shutdownTasks); } } @@ -318,6 +318,17 @@ private boolean isIntegrationTest(Class clazz) { @Override public void afterAll(ExtensionContext context) throws Exception { currentTestClassStack.pop(); + + try { + if (shutdownTasks != null) { + for (Runnable shutdownTask : shutdownTasks) { + shutdownTask.run(); + } + } + shutdownTasks = null; + } catch (Exception e) { + System.err.println("Unable to run shutdown tasks: " + e.getMessage()); + } } @Override From 3ddf2a81ecd88d4d07e424ed71bc57daf961be51 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Sat, 23 Nov 2024 08:18:03 -0500 Subject: [PATCH 21/38] Remove line (cherry picked from commit 522335a5cd910257ec34ace0ebeab2d3d35346b7) --- docs/src/main/asciidoc/security-jwt-build.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/main/asciidoc/security-jwt-build.adoc b/docs/src/main/asciidoc/security-jwt-build.adoc index de94662e7a8e9..931ebdf339139 100644 --- a/docs/src/main/asciidoc/security-jwt-build.adoc +++ b/docs/src/main/asciidoc/security-jwt-build.adoc @@ -32,8 +32,6 @@ To use the SmallRye JWT Build API, add the following dependency to your project: ---- -Alternatively, for a Gradle-based project, use the following configuration: - [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- From 57418d47ac114b501f2cba5392f60d5b848f25a5 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Thu, 21 Nov 2024 17:54:23 +0000 Subject: [PATCH 22/38] Split creation of the Set to match unknown properties to avoid MethodTooLargeException (cherry picked from commit 1db73a7f9890b10975b90335ca5a51fedbfc220d) --- .../quarkus/deployment/ExtensionLoader.java | 2 +- .../BuildTimeConfigurationReader.java | 48 ++++++++++++++--- .../RunTimeConfigurationGenerator.java | 51 +++++++++++-------- .../runtime/configuration/PropertiesUtil.java | 28 ---------- 4 files changed, 72 insertions(+), 57 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java index 963027bc89c1b..a65691e60b8da 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java @@ -182,7 +182,7 @@ public String getId() { } // ConfigMapping - ConfigClass mapping = readResult.getAllMappings().get(entry.getKey()); + ConfigClass mapping = readResult.getAllMappingsByClass().get(entry.getKey()); if (mapping != null) { mappingClasses.put(entry.getValue(), mapping); continue; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index b160e70246a94..052ee5e5a0e52 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -628,9 +628,9 @@ ReadResult run() { objectsByClass.put(mapping.getKlass(), config.getConfigMapping(mapping.getKlass(), mapping.getPrefix())); } - Set buildTimeNames = getMappingsNames(buildTimeMappings); - Set buildTimeRunTimeNames = getMappingsNames(buildTimeRunTimeMappings); - Set runTimeNames = getMappingsNames(runTimeMappings); + Set buildTimeNames = mappingsToNames(buildTimeMappings).keySet(); + Set buildTimeRunTimeNames = mappingsToNames(buildTimeRunTimeMappings).keySet(); + Set runTimeNames = mappingsToNames(runTimeMappings).keySet(); for (String property : allProperties) { PropertyName name = new PropertyName(property); if (buildTimeNames.contains(name)) { @@ -1222,12 +1222,32 @@ private static void getDefaults( } } - private static Set getMappingsNames(final List configMappings) { + private static Map mappingsToNames(final List configMappings) { Set names = new HashSet<>(); for (ConfigClass configMapping : configMappings) { names.addAll(ConfigMappings.getProperties(configMapping).keySet()); } - return PropertiesUtil.toPropertyNames(names); + Map propertyNames = new HashMap<>(); + for (String name : names) { + PropertyName propertyName = new PropertyName(name); + if (propertyNames.containsKey(propertyName)) { + String existing = propertyNames.remove(propertyName); + if (existing.length() < name.length()) { + propertyNames.put(new PropertyName(existing), existing); + } else if (existing.length() > name.length()) { + propertyNames.put(propertyName, name); + } else { + if (existing.indexOf('*') <= name.indexOf('*')) { + propertyNames.put(new PropertyName(existing), existing); + } else { + propertyNames.put(propertyName, name); + } + } + } else { + propertyNames.put(propertyName, name); + } + } + return propertyNames; } } @@ -1249,7 +1269,9 @@ public static final class ReadResult { final List buildTimeMappings; final List buildTimeRunTimeMappings; final List runTimeMappings; - final Map, ConfigClass> allMappings; + final List allMappings; + final Map, ConfigClass> allMappingsByClass; + final Map allMappingsNames; final Set unknownBuildProperties; final Set deprecatedRuntimeProperties; @@ -1273,7 +1295,9 @@ public ReadResult(final Builder builder) { this.buildTimeMappings = builder.getBuildTimeMappings(); this.buildTimeRunTimeMappings = builder.getBuildTimeRunTimeMappings(); this.runTimeMappings = builder.getRunTimeMappings(); - this.allMappings = mappingsToMap(builder); + this.allMappings = new ArrayList<>(mappingsToMap(builder).values()); + this.allMappingsByClass = mappingsToMap(builder); + this.allMappingsNames = ReadOperation.mappingsToNames(allMappings); this.unknownBuildProperties = builder.getUnknownBuildProperties(); this.deprecatedRuntimeProperties = builder.deprecatedRuntimeProperties; @@ -1355,10 +1379,18 @@ public List getRunTimeMappings() { return runTimeMappings; } - public Map, ConfigClass> getAllMappings() { + public List getAllMappings() { return allMappings; } + public Map, ConfigClass> getAllMappingsByClass() { + return allMappingsByClass; + } + + public Map getAllMappingsNames() { + return allMappingsNames; + } + public Set getUnknownBuildProperties() { return unknownBuildProperties; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index 0d8e72863855b..1df607c7ba24e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -6,6 +6,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -63,6 +64,7 @@ import io.quarkus.runtime.configuration.NameIterator; import io.quarkus.runtime.configuration.PropertiesUtil; import io.quarkus.runtime.configuration.QuarkusConfigFactory; +import io.smallrye.config.ConfigMappingInterface; import io.smallrye.config.ConfigMappings; import io.smallrye.config.ConfigMappings.ConfigClass; import io.smallrye.config.Converters; @@ -88,9 +90,6 @@ public final class RunTimeConfigurationGenerator { public static final MethodDescriptor C_READ_CONFIG = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "readConfig", void.class); static final FieldDescriptor C_MAPPED_PROPERTIES = FieldDescriptor.of(CONFIG_CLASS_NAME, "mappedProperties", Set.class); - static final MethodDescriptor C_GENERATE_MAPPED_PROPERTIES = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, - "generateMappedProperties", Set.class); - static final MethodDescriptor PN_NEW = MethodDescriptor.ofConstructor(PropertyName.class, String.class); static final FieldDescriptor C_UNKNOWN = FieldDescriptor.of(CONFIG_CLASS_NAME, "unknown", Set.class); static final FieldDescriptor C_UNKNOWN_RUNTIME = FieldDescriptor.of(CONFIG_CLASS_NAME, "unknownRuntime", Set.class); @@ -192,9 +191,13 @@ public final class RunTimeConfigurationGenerator { static final MethodDescriptor PU_IS_PROPERTY_IN_ROOTS = MethodDescriptor.ofMethod(PropertiesUtil.class, "isPropertyInRoots", boolean.class, String.class, Set.class); static final MethodDescriptor HS_NEW = MethodDescriptor.ofConstructor(HashSet.class); + static final MethodDescriptor HS_NEW_SIZED = MethodDescriptor.ofConstructor(HashSet.class, int.class); static final MethodDescriptor HS_ADD = MethodDescriptor.ofMethod(HashSet.class, "add", boolean.class, Object.class); + static final MethodDescriptor HS_ADD_ALL = MethodDescriptor.ofMethod(HashSet.class, "addAll", boolean.class, + Collection.class); static final MethodDescriptor HS_CONTAINS = MethodDescriptor.ofMethod(HashSet.class, "contains", boolean.class, Object.class); + static final MethodDescriptor PN_NEW = MethodDescriptor.ofConstructor(PropertyName.class, String.class); // todo: more space-efficient sorted map impl static final MethodDescriptor TM_NEW = MethodDescriptor.ofConstructor(TreeMap.class); @@ -262,7 +265,6 @@ public static final class GenerateOperation implements AutoCloseable { roots = Assert.checkNotNullParam("builder.roots", builder.getBuildTimeReadResult().getAllRoots()); additionalTypes = Assert.checkNotNullParam("additionalTypes", builder.getAdditionalTypes()); cc = ClassCreator.builder().classOutput(classOutput).className(CONFIG_CLASS_NAME).setFinal(true).build(); - generateMappedProperties(); generateEmptyParsers(); // not instantiable try (MethodCreator mc = cc.getMethodCreator(MethodDescriptor.ofConstructor(CONFIG_CLASS_NAME))) { @@ -282,7 +284,8 @@ public static final class GenerateOperation implements AutoCloseable { clinit.setModifiers(Opcodes.ACC_STATIC); cc.getFieldCreator(C_MAPPED_PROPERTIES).setModifiers(Opcodes.ACC_STATIC); - clinit.writeStaticField(C_MAPPED_PROPERTIES, clinit.invokeStaticMethod(C_GENERATE_MAPPED_PROPERTIES)); + clinit.writeStaticField(C_MAPPED_PROPERTIES, clinit.newInstance(HS_NEW_SIZED, + clinit.load((int) ((float) buildTimeConfigResult.getAllMappingsNames().size() / 0.75f + 1.0f)))); cc.getFieldCreator(C_UNKNOWN).setModifiers(Opcodes.ACC_STATIC); clinit.writeStaticField(C_UNKNOWN, clinit.newInstance(HS_NEW)); @@ -290,6 +293,8 @@ public static final class GenerateOperation implements AutoCloseable { cc.getFieldCreator(C_UNKNOWN_RUNTIME).setModifiers(Opcodes.ACC_STATIC); clinit.writeStaticField(C_UNKNOWN_RUNTIME, clinit.newInstance(HS_NEW)); + generateMappedProperties(); + clinitNameBuilder = clinit.newInstance(SB_NEW); // the build time config, which is for user use only (not used by us other than for loading converters) @@ -1181,26 +1186,32 @@ private FieldDescriptor getOrCreateConverterInstance(Field field, ConverterType } private void generateMappedProperties() { - Set names = new HashSet<>(); - for (ConfigClass buildTimeMapping : buildTimeConfigResult.getBuildTimeMappings()) { - names.addAll(ConfigMappings.getProperties(buildTimeMapping).keySet()); - } - for (ConfigClass staticConfigMapping : buildTimeConfigResult.getBuildTimeRunTimeMappings()) { - names.addAll(ConfigMappings.getProperties(staticConfigMapping).keySet()); - } - for (ConfigClass runtimeConfigMapping : buildTimeConfigResult.getRunTimeMappings()) { - names.addAll(ConfigMappings.getProperties(runtimeConfigMapping).keySet()); + MethodDescriptor method = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "addMappedProperties", void.class); + MethodCreator mc = cc.getMethodCreator(method); + mc.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); + for (ConfigClass mapping : buildTimeConfigResult.getAllMappings()) { + mc.invokeStaticMethod(generateMappedProperties(mapping, buildTimeConfigResult.getAllMappingsNames())); } - Set propertyNames = PropertiesUtil.toPropertyNames(names); + mc.returnVoid(); + mc.close(); + clinit.invokeStaticMethod(method); + } - MethodCreator mc = cc.getMethodCreator(C_GENERATE_MAPPED_PROPERTIES); + private MethodDescriptor generateMappedProperties(final ConfigClass mapping, + final Map propertyNames) { + MethodDescriptor method = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + "addMappedProperties$" + mapping.getKlass().getName().replace('.', '$'), void.class); + MethodCreator mc = cc.getMethodCreator(method); mc.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); - ResultHandle set = mc.newInstance(HS_NEW); - for (PropertyName propertyName : propertyNames) { - mc.invokeVirtualMethod(HS_ADD, set, mc.newInstance(PN_NEW, mc.load(propertyName.getName()))); + Map properties = ConfigMappings.getProperties(mapping); + ResultHandle set = mc.readStaticField(C_MAPPED_PROPERTIES); + for (String propertyName : properties.keySet()) { + String name = propertyNames.get(new PropertyName(propertyName)); + mc.invokeVirtualMethod(HS_ADD, set, mc.newInstance(PN_NEW, mc.load(name))); } - mc.returnValue(set); + mc.returnVoid(); mc.close(); + return method; } private void reportUnknown(BytecodeCreator bc, ResultHandle unknownProperty) { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java index 18978d1714028..7c51a8707538e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java @@ -1,11 +1,7 @@ package io.quarkus.runtime.configuration; -import java.util.HashMap; -import java.util.Map; import java.util.Set; -import io.smallrye.config.PropertyName; - public class PropertiesUtil { private PropertiesUtil() { throw new IllegalStateException("Utility class"); @@ -48,28 +44,4 @@ public static boolean isPropertyInRoot(final String property, final String root) public static boolean isPropertyQuarkusCompoundName(NameIterator propertyName) { return propertyName.getName().startsWith("\"quarkus."); } - - public static Set toPropertyNames(final Set names) { - Map propertyNames = new HashMap<>(); - for (String name : names) { - PropertyName propertyName = new PropertyName(name); - if (propertyNames.containsKey(propertyName)) { - String existing = propertyNames.remove(propertyName); - if (existing.length() < name.length()) { - propertyNames.put(new PropertyName(existing), existing); - } else if (existing.length() > name.length()) { - propertyNames.put(propertyName, name); - } else { - if (existing.indexOf('*') <= name.indexOf('*')) { - propertyNames.put(new PropertyName(existing), existing); - } else { - propertyNames.put(propertyName, name); - } - } - } else { - propertyNames.put(propertyName, name); - } - } - return propertyNames.keySet(); - } } From f68ab7248d4bb6c6d7e04e4da3a20c171b1c4734 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 25 Nov 2024 10:41:14 +0200 Subject: [PATCH 23/38] Properly reset Quarkus populated Jackson ObjectMapper Fixes: #44641 (cherry picked from commit a0332318d71bf2507d99a8fe8c39e0e0978ed2b0) --- .../io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java index 8847fcfb59f30..773c7831391c5 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java @@ -20,6 +20,7 @@ public JsonCodec codec() { try { // First try the Quarkus databind codec codec = new QuarkusJacksonJsonCodec(); + COUNTER.incrementAndGet(); } catch (Throwable t1) { // Then try the Vert.x databind codec try { @@ -29,7 +30,6 @@ public JsonCodec codec() { codec = new JacksonCodec(); } } - COUNTER.incrementAndGet(); return codec; } From 52234d8ce7eff1ad20781e27b99e1b90f07e2752 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 25 Nov 2024 11:08:06 +0100 Subject: [PATCH 24/38] Qute: fix handling of missing properties in strict mode - fixes #44674 (cherry picked from commit dd015311e5a7d6f35aeadf69fb2f22cc92c57d96) --- .../java/io/quarkus/qute/CompletedStage.java | 6 +- .../java/io/quarkus/qute/EvaluatorImpl.java | 6 +- .../java/io/quarkus/qute/LetTimeoutTest.java | 79 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 independent-projects/qute/core/src/test/java/io/quarkus/qute/LetTimeoutTest.java diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/CompletedStage.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/CompletedStage.java index 372fa9df306de..27d7ee9451ce7 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/CompletedStage.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/CompletedStage.java @@ -61,7 +61,7 @@ public boolean isFailure() { public T get() { if (exception != null) { - // Throw an exception if completed exceptionally + // Always wrap the original exception if completed exceptionally throw new TemplateException(exception); } return result; @@ -285,8 +285,12 @@ public CompletionStage whenComplete(BiConsumer Objects.requireNonNull(action).accept(result, exception); } catch (Throwable e) { if (exception == null) { + // "if this stage completed normally but the supplied action throws an exception, + // then the returned stage completes exceptionally with the supplied action's exception" return new CompletedStage<>(null, e); } + // if this stage completed exceptionally and the supplied action throws an exception, + // then the returned stage completes exceptionally with this stage's exception } return this; } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java index 3724328368b19..590f9617a5fe9 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java @@ -109,7 +109,7 @@ private CompletionStage resolveNamespace(EvalContext context, Resolution // Continue to the next part of the expression return resolveReference(false, r, parts, resolutionContext, expression, 1); } else if (strictRendering) { - throw propertyNotFound(r, expression); + return CompletedStage.failure(propertyNotFound(r, expression)); } return Results.notFound(context); } @@ -199,9 +199,9 @@ private CompletionStage resolve(EvalContextImpl evalContext, Iteratorhttps://github.com/quarkusio/quarkus/issues/44674. + */ +public class LetTimeoutTest { + + Engine engine = Engine.builder().addDefaults().build(); + + private static final Map DATA = Map.of( + "a", Map.of("b", Map.of())); + + @Test + void withDataFactoryMethod() { + TemplateInstance instance = engine.parse(""" + {#let b = a.b} + {c} + {/let} + """).data(DATA); + + assertThatThrownBy(instance::render) + .isInstanceOf(TemplateException.class) + .hasRootCauseMessage("Rendering error: Key \"c\" not found in the map with keys [a] in expression {c}"); + } + + @Test + void withInstanceThenDataForEachEntry() { + TemplateInstance instance = engine.parse(""" + {#let b=a.b} + {c} + {/let} + """).instance(); + for (var e : DATA.entrySet()) { + instance.data(e.getKey(), e.getValue()); + } + assertThatThrownBy(instance::render) + .isInstanceOf(TemplateException.class) + .hasRootCauseMessage( + "Rendering error: Key \"c\" not found in the template data map with keys [a] in expression {c}"); + } + + @Test + void withSet_withInstanceThenDataForEachEntry() { + TemplateInstance instance = engine.parse(""" + {#set b = a.b} + {c} + {/set} + """).instance(); + for (var e : DATA.entrySet()) { + instance.data(e.getKey(), e.getValue()); + } + assertThatThrownBy(instance::render) + .isInstanceOf(TemplateException.class) + .hasRootCauseMessage( + "Rendering error: Key \"c\" not found in the template data map with keys [a] in expression {c}"); + } + + @Test + void withLetWithoutEndTagwithInstanceThenDataForEachEntry() { + TemplateInstance instance = engine.parse(""" + {#let b = a.b} + {c} + """).instance(); + for (var e : DATA.entrySet()) { + instance.data(e.getKey(), e.getValue()); + } + assertThatThrownBy(instance::render) + .isInstanceOf(TemplateException.class) + .hasRootCauseMessage( + "Rendering error: Key \"c\" not found in the template data map with keys [a] in expression {c}"); + } +} From 86148cef8f87d940855b9ff00716ac27b0923138 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Mon, 25 Nov 2024 10:52:47 +0200 Subject: [PATCH 25/38] Fix glob to regex conversation to properly handle **/*.suffix (cherry picked from commit e9d1674ac7a63b045224825fd1fdbdd428902992) --- .../app-model/src/main/java/io/quarkus/util/GlobUtil.java | 7 ++++++- .../src/test/java/io/quarkus/util/GlobUtilTest.java | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/util/GlobUtil.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/util/GlobUtil.java index 84ce28d1a1ae7..e717aea3b5ee1 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/util/GlobUtil.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/util/GlobUtil.java @@ -75,8 +75,13 @@ private static int glob(String glob, int i, int length, String stopChars, String switch (current) { case '*': if (i < length && glob.charAt(i) == '*') { - result.append(".*"); i++; + if (i < length && glob.charAt(i) == '/') { + result.append("([^/]*/)*"); + i++; + } else { + result.append(".*"); + } } else { result.append("[^/]*"); } diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/util/GlobUtilTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/util/GlobUtilTest.java index 7ec0ebecdd943..8e47c141af0d3 100644 --- a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/util/GlobUtilTest.java +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/util/GlobUtilTest.java @@ -34,6 +34,14 @@ void doubleStar() { assertMatch("a**/b", Arrays.asList("a/b", "axy/b", "a/x/b"), Arrays.asList("a", "b", "a/bc", "bc/b")); assertMatch("a**b", Arrays.asList("ab", "axb", "axyb", "a/b", "a/x/b"), Arrays.asList("abc", "1ab")); assertMatch("a**b**/c", Arrays.asList("axbx/c", "axbx/c", "a/x/xbx/c", "axbx/xxx/c"), Arrays.asList("axbx/cc")); + assertMatch("**/*.txt", Arrays.asList("/test.txt", "test.txt", "/path/to/a.txt", "relative/path/to/a.txt"), + Arrays.asList("/test.py", "test.json", "/path/to/a.js", "relative/path/to/a.exe")); + assertMatch("foo/**/test.json", + Arrays.asList("foo/a/b/vd/test.json", "foo/test.json", "foo/42/test.json"), + Arrays.asList("/foo/path/to/test.json", "/test.py", "test.json", "/path/foo/test.json", "path/foo/test.json")); + assertMatch("foo/**/*.json", + Arrays.asList("foo/a/b/vd/a.json", "foo/dsa.json", "foo/32/test2.json"), + Arrays.asList("/foo/path/to/aasf.json", "/test.py", "test.json", "/path/foo/test.json", "path/foo/test.json")); } @Test From 96eadf1b396ba1818e336af743228f0eb3764248 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Sat, 23 Nov 2024 18:30:04 +0100 Subject: [PATCH 26/38] Config Doc - Fix conversion of HTML Javadoc Converting HTML to Asciidoc using the Javadoc elements was causing issues as the HTML would be split and the fragments would be handled without the knowledge of the HTML tree. (cherry picked from commit 916d1947af1669a5d219d1f840eae45a67a007f7) --- .../JavadocToAsciidocTransformer.java | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index 5007f9ce71bc0..8e2f07e13ddab 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -1,5 +1,8 @@ package io.quarkus.annotation.processor.documentation.config.formatter; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jsoup.Jsoup; @@ -62,6 +65,8 @@ public final class JavadocToAsciidocTransformer { private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE = "[quote]\n____"; private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END = "____"; + private static final Pattern INLINE_TAG_MARKER_PATTERN = Pattern.compile("§§([0-9]+)§§"); + private JavadocToAsciidocTransformer() { } @@ -84,7 +89,11 @@ public static String toAsciidoc(String javadoc, JavadocFormat format, boolean in // we add it as it has been previously removed Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); + StringBuilder htmlJavadoc = new StringBuilder(javadoc.length()); + + int markerCounter = 0; StringBuilder sb = new StringBuilder(); + Map inlineTagsReplacements = new TreeMap<>(); for (JavadocDescriptionElement javadocDescriptionElement : parsedJavadoc.getDescription().getElements()) { if (javadocDescriptionElement instanceof JavadocInlineTag) { @@ -95,40 +104,56 @@ public static String toAsciidoc(String javadoc, JavadocFormat format, boolean in case VALUE: case LITERAL: case SYSTEM_PROPERTY: + sb.setLength(0); sb.append('`'); appendEscapedAsciiDoc(sb, content, inlineMacroMode); sb.append('`'); + htmlJavadoc.append("§§" + markerCounter + "§§"); + inlineTagsReplacements.put(markerCounter, sb.toString()); + markerCounter++; break; case LINK: case LINKPLAIN: if (content.startsWith(HASH)) { content = ConfigNamingUtil.hyphenate(content.substring(1)); } + sb.setLength(0); sb.append('`'); appendEscapedAsciiDoc(sb, content, inlineMacroMode); sb.append('`'); + htmlJavadoc.append("§§" + markerCounter + "§§"); + inlineTagsReplacements.put(markerCounter, sb.toString()); + markerCounter++; break; default: - sb.append(content); + htmlJavadoc.append(content); break; } } else { - appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText()), inlineMacroMode); + htmlJavadoc.append(javadocDescriptionElement.toText()); } } - String asciidoc = trim(sb); + StringBuilder asciidocSb = new StringBuilder(); + htmlToAsciidoc(asciidocSb, Jsoup.parseBodyFragment(htmlJavadoc.toString()), inlineMacroMode); + String asciidoc = trim(asciidocSb); + + // not very optimal and could be included in htmlToAsciidoc() but simpler so let's go for it + if (!inlineTagsReplacements.isEmpty()) { + asciidoc = INLINE_TAG_MARKER_PATTERN.matcher(asciidoc) + .replaceAll(mr -> Matcher.quoteReplacement(inlineTagsReplacements.get(Integer.valueOf(mr.group(1))))); + } return asciidoc.isBlank() ? null : asciidoc; } - private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroMode) { + private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMacroMode) { for (Node childNode : node.childNodes()) { switch (childNode.nodeName()) { case PARAGRAPH_NODE: newLine(sb); newLine(sb); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); break; case PREFORMATED_NODE: newLine(sb); @@ -148,7 +173,7 @@ private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroM newLine(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE); newLine(sb); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); newLineIfNeeded(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END); newLine(sb); @@ -157,67 +182,68 @@ private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroM case ORDERED_LIST_NODE: case UN_ORDERED_LIST_NODE: newLine(sb); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); + newLine(sb); break; case LIST_ITEM_NODE: - final String marker = childNode.parent().nodeName().equals(ORDERED_LIST_NODE) + final String marker = childNode.parentNode().nodeName().equals(ORDERED_LIST_NODE) ? ORDERED_LIST_ITEM_ASCIDOC_STYLE : UNORDERED_LIST_ITEM_ASCIDOC_STYLE; newLine(sb); sb.append(marker); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); break; case LINK_NODE: final String link = childNode.attr(HREF_ATTRIBUTE); sb.append("link:"); sb.append(link); final StringBuilder caption = new StringBuilder(); - appendHtml(caption, childNode, inlineMacroMode); + htmlToAsciidoc(caption, childNode, inlineMacroMode); sb.append(String.format(LINK_ATTRIBUTE_FORMAT, trim(caption))); break; case CODE_NODE: sb.append(BACKTICK); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(BACKTICK); break; case BOLD_NODE: case STRONG_NODE: sb.append(STAR); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(STAR); break; case EMPHASIS_NODE: case ITALICS_NODE: sb.append(UNDERSCORE); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(UNDERSCORE); break; case UNDERLINE_NODE: sb.append(UNDERLINE_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(HASH); break; case SMALL_NODE: sb.append(SMALL_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(HASH); break; case BIG_NODE: sb.append(BIG_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(HASH); break; case SUB_SCRIPT_NODE: sb.append(SUB_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(SUB_SCRIPT_ASCIDOC_STYLE); break; case SUPER_SCRIPT_NODE: sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); break; case DEL_NODE: @@ -225,7 +251,7 @@ private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroM case STRIKE_NODE: sb.append(LINE_THROUGH_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); sb.append(HASH); break; case NEW_LINE_NODE: @@ -249,7 +275,7 @@ private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroM appendEscapedAsciiDoc(sb, text, inlineMacroMode); break; default: - appendHtml(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode); break; } } From ad78ba7ee1dd015f6efbc1ca603ab3696d074193 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 25 Nov 2024 10:08:00 +0100 Subject: [PATCH 27/38] Config Doc - Add basic support for HTML tables -> AsciiDoc tables It will only handle very simple cases but it makes the doc for quarkus.native.resources.includes a lot better. (cherry picked from commit 10ddc12e3931b75d701f2c3707361a294913fbda) --- .../JavadocToAsciidocTransformer.java | 130 +++++++++++++++--- 1 file changed, 109 insertions(+), 21 deletions(-) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index 8e2f07e13ddab..adf98de79d86c 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -50,6 +50,12 @@ public final class JavadocToAsciidocTransformer { private static final String UN_ORDERED_LIST_NODE = "ul"; private static final String PREFORMATED_NODE = "pre"; private static final String BLOCKQUOTE_NODE = "blockquote"; + private static final String TABLE_NODE = "table"; + private static final String THEAD_NODE = "thead"; + private static final String TBODY_NODE = "tbody"; + private static final String TR_NODE = "tr"; + private static final String TH_NODE = "th"; + private static final String TD_NODE = "td"; private static final String BIG_ASCIDOC_STYLE = "[.big]"; private static final String LINK_ATTRIBUTE_FORMAT = "[%s]"; @@ -64,6 +70,9 @@ public final class JavadocToAsciidocTransformer { private static final String CODE_BLOCK_ASCIDOC_STYLE = "```"; private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE = "[quote]\n____"; private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END = "____"; + private static final String TABLE_MARKER = "!==="; + private static final String COLUMN_HEADER_MARKER = "h!"; + private static final String COLUMN_MARKER = "!"; private static final Pattern INLINE_TAG_MARKER_PATTERN = Pattern.compile("§§([0-9]+)§§"); @@ -106,7 +115,7 @@ public static String toAsciidoc(String javadoc, JavadocFormat format, boolean in case SYSTEM_PROPERTY: sb.setLength(0); sb.append('`'); - appendEscapedAsciiDoc(sb, content, inlineMacroMode); + appendEscapedAsciiDoc(sb, content, inlineMacroMode, new Context()); sb.append('`'); htmlJavadoc.append("§§" + markerCounter + "§§"); inlineTagsReplacements.put(markerCounter, sb.toString()); @@ -119,7 +128,7 @@ public static String toAsciidoc(String javadoc, JavadocFormat format, boolean in } sb.setLength(0); sb.append('`'); - appendEscapedAsciiDoc(sb, content, inlineMacroMode); + appendEscapedAsciiDoc(sb, content, inlineMacroMode, new Context()); sb.append('`'); htmlJavadoc.append("§§" + markerCounter + "§§"); inlineTagsReplacements.put(markerCounter, sb.toString()); @@ -135,7 +144,7 @@ public static String toAsciidoc(String javadoc, JavadocFormat format, boolean in } StringBuilder asciidocSb = new StringBuilder(); - htmlToAsciidoc(asciidocSb, Jsoup.parseBodyFragment(htmlJavadoc.toString()), inlineMacroMode); + htmlToAsciidoc(asciidocSb, Jsoup.parseBodyFragment(htmlJavadoc.toString()), inlineMacroMode, new Context()); String asciidoc = trim(asciidocSb); // not very optimal and could be included in htmlToAsciidoc() but simpler so let's go for it @@ -147,13 +156,13 @@ public static String toAsciidoc(String javadoc, JavadocFormat format, boolean in return asciidoc.isBlank() ? null : asciidoc; } - private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMacroMode) { + private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMacroMode, Context context) { for (Node childNode : node.childNodes()) { switch (childNode.nodeName()) { case PARAGRAPH_NODE: newLine(sb); newLine(sb); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); break; case PREFORMATED_NODE: newLine(sb); @@ -173,7 +182,7 @@ private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMa newLine(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE); newLine(sb); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); newLineIfNeeded(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END); newLine(sb); @@ -182,7 +191,7 @@ private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMa case ORDERED_LIST_NODE: case UN_ORDERED_LIST_NODE: newLine(sb); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); newLine(sb); break; case LIST_ITEM_NODE: @@ -191,59 +200,59 @@ private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMa : UNORDERED_LIST_ITEM_ASCIDOC_STYLE; newLine(sb); sb.append(marker); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); break; case LINK_NODE: final String link = childNode.attr(HREF_ATTRIBUTE); sb.append("link:"); sb.append(link); final StringBuilder caption = new StringBuilder(); - htmlToAsciidoc(caption, childNode, inlineMacroMode); + htmlToAsciidoc(caption, childNode, inlineMacroMode, context); sb.append(String.format(LINK_ATTRIBUTE_FORMAT, trim(caption))); break; case CODE_NODE: sb.append(BACKTICK); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(BACKTICK); break; case BOLD_NODE: case STRONG_NODE: sb.append(STAR); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(STAR); break; case EMPHASIS_NODE: case ITALICS_NODE: sb.append(UNDERSCORE); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(UNDERSCORE); break; case UNDERLINE_NODE: sb.append(UNDERLINE_ASCIDOC_STYLE); sb.append(HASH); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(HASH); break; case SMALL_NODE: sb.append(SMALL_ASCIDOC_STYLE); sb.append(HASH); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(HASH); break; case BIG_NODE: sb.append(BIG_ASCIDOC_STYLE); sb.append(HASH); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(HASH); break; case SUB_SCRIPT_NODE: sb.append(SUB_SCRIPT_ASCIDOC_STYLE); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(SUB_SCRIPT_ASCIDOC_STYLE); break; case SUPER_SCRIPT_NODE: sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); break; case DEL_NODE: @@ -251,7 +260,7 @@ private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMa case STRIKE_NODE: sb.append(LINE_THROUGH_ASCIDOC_STYLE); sb.append(HASH); - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); sb.append(HASH); break; case NEW_LINE_NODE: @@ -272,10 +281,58 @@ private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMa text = startingSpaceMatcher.replaceFirst(""); } - appendEscapedAsciiDoc(sb, text, inlineMacroMode); + appendEscapedAsciiDoc(sb, text, inlineMacroMode, context); + break; + case TABLE_NODE: + newLine(sb); + newLine(sb); + sb.append(TABLE_MARKER); + newLine(sb); + context.inTable = true; + context.firstTableRow = true; + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); + context.inTable = false; + context.firstTableRow = false; + sb.append(TABLE_MARKER); + newLine(sb); + break; + case THEAD_NODE: + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); + break; + case TBODY_NODE: + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); + break; + case TR_NODE: + trimTrailingWhitespaces(sb); + if (!context.firstTableRow) { + newLine(sb); + } + newLine(sb); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); + context.firstTableRow = false; + break; + case TH_NODE: + if (!context.firstTableRow) { + sb.append(COLUMN_HEADER_MARKER); + } else { + sb.append(COLUMN_MARKER); + } + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); + trimTrailingWhitespaces(sb); + if (!context.firstTableRow) { + newLine(sb); + } + break; + case TD_NODE: + sb.append(COLUMN_MARKER); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); + trimTrailingWhitespaces(sb); + if (!context.firstTableRow) { + newLine(sb); + } break; default: - htmlToAsciidoc(sb, childNode, inlineMacroMode); + htmlToAsciidoc(sb, childNode, inlineMacroMode, context); break; } } @@ -351,6 +408,20 @@ private static StringBuilder trimText(StringBuilder sb, String charsToTrim) { return sb; } + private static void trimTrailingWhitespaces(StringBuilder sb) { + int j = -1; + for (int i = sb.length() - 1; i >= 0; i--) { + if (Character.isWhitespace(sb.charAt(i))) { + j = i; + } else { + break; + } + } + if (j >= 0) { + sb.setLength(j); + } + } + private static StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { int i = 0; /* trim leading whitespace */ @@ -417,7 +488,8 @@ private static StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) return sb; } - private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text, boolean inlineMacroMode) { + private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text, boolean inlineMacroMode, + Context context) { boolean escaping = false; for (int i = 0; i < text.length(); i++) { final char ch = text.charAt(i); @@ -453,6 +525,16 @@ private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text } sb.append("{plus}"); break; + case '!': + if (escaping) { + sb.append("++"); + escaping = false; + } + if (context.inTable) { + sb.append('\\'); + } + sb.append(ch); + break; default: if (escaping) { sb.append("++"); @@ -466,4 +548,10 @@ private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text } return sb; } + + private static class Context { + + boolean inTable; + boolean firstTableRow; + } } From a475cd8a32e4bc62be976a5f290e6e01d4ac1ccc Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 25 Nov 2024 12:38:07 +0100 Subject: [PATCH 28/38] Config Doc - Enforce a new line after ol/ul (cherry picked from commit c4ba187110f0bec6e8f5a240182cfb3c9f873519) --- .../config/formatter/JavadocToAsciidocTransformer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index adf98de79d86c..19a23c1a0427d 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -193,6 +193,7 @@ private static void htmlToAsciidoc(StringBuilder sb, Node node, boolean inlineMa newLine(sb); htmlToAsciidoc(sb, childNode, inlineMacroMode, context); newLine(sb); + newLine(sb); break; case LIST_ITEM_NODE: final String marker = childNode.parentNode().nodeName().equals(ORDERED_LIST_NODE) From 7b1b0c90a5de7698bfc8c9248eb70bd89a162b36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:28:17 +0000 Subject: [PATCH 29/38] Bump hibernate-search.version from 7.2.1.Final to 7.2.2.Final Bumps `hibernate-search.version` from 7.2.1.Final to 7.2.2.Final. Updates `org.hibernate.search:hibernate-search-bom` from 7.2.1.Final to 7.2.2.Final - [Release notes](https://github.com/hibernate/hibernate-search/releases) - [Changelog](https://github.com/hibernate/hibernate-search/blob/7.2.2.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-search/compare/7.2.1.Final...7.2.2.Final) Updates `org.hibernate.search:hibernate-search-mapper-orm` from 7.2.1.Final to 7.2.2.Final - [Release notes](https://github.com/hibernate/hibernate-search/releases) - [Changelog](https://github.com/hibernate/hibernate-search/blob/7.2.2.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-search/compare/7.2.1.Final...7.2.2.Final) Updates `org.hibernate.search:hibernate-search-engine` from 7.2.1.Final to 7.2.2.Final - [Release notes](https://github.com/hibernate/hibernate-search/releases) - [Changelog](https://github.com/hibernate/hibernate-search/blob/7.2.2.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-search/compare/7.2.1.Final...7.2.2.Final) Updates `org.hibernate.search:hibernate-search-mapper-pojo-base` from 7.2.1.Final to 7.2.2.Final - [Release notes](https://github.com/hibernate/hibernate-search/releases) - [Changelog](https://github.com/hibernate/hibernate-search/blob/7.2.2.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-search/compare/7.2.1.Final...7.2.2.Final) Updates `org.hibernate.search:hibernate-search-util-common` from 7.2.1.Final to 7.2.2.Final - [Release notes](https://github.com/hibernate/hibernate-search/releases) - [Changelog](https://github.com/hibernate/hibernate-search/blob/7.2.2.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-search/compare/7.2.1.Final...7.2.2.Final) --- updated-dependencies: - dependency-name: org.hibernate.search:hibernate-search-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.hibernate.search:hibernate-search-mapper-orm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.hibernate.search:hibernate-search-engine dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.hibernate.search:hibernate-search-mapper-pojo-base dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.hibernate.search:hibernate-search-util-common dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 5ec309fa3b5f8f9b0f14d33d4eee46ef3f1bc60b) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f2f38e6ee861d..e9fc13e768a25 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 7.0.3.Final 2.4.2.Final 8.0.1.Final - 7.2.1.Final + 7.2.2.Final 1.65.1 From 9e981249bbfa29f242e4bb8a8562194aab5eeee0 Mon Sep 17 00:00:00 2001 From: xstefank Date: Tue, 26 Nov 2024 14:27:03 +0100 Subject: [PATCH 30/38] Fix leftover -reactive properties references in REST client guide (cherry picked from commit 622672c02f3f52e288bb406711967f885be7b078) --- docs/src/main/asciidoc/rest-client.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index c565f2328c955..5cb6e2b44395c 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1340,7 +1340,7 @@ public class MyResponseExceptionMapper implements ResponseExceptionMapper Date: Tue, 26 Nov 2024 10:09:57 +0100 Subject: [PATCH 31/38] Fix TLS config Javadoc typo Noticed while working on the management interface. (cherry picked from commit d59ea8fbccb9ee113b00dde24cf71e497fdccbaf) --- .../java/io/quarkus/redis/runtime/client/config/TlsConfig.java | 2 +- .../java/io/quarkus/tls/runtime/config/TlsBucketConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/TlsConfig.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/TlsConfig.java index e8a6c46a75e95..55d83d4198d31 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/TlsConfig.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/TlsConfig.java @@ -66,7 +66,7 @@ public interface TlsConfig { /** * The hostname verification algorithm to use in case the server's identity should be checked. - * Should be {@code HTTPS}, {@code LDAPS} or an {@code NONE} (default). + * Should be {@code HTTPS}, {@code LDAPS} or {@code NONE} (default). *

* If set to {@code NONE}, it does not verify the hostname. *

diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java index a45c6aa0534be..47eab60a8def9 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java @@ -97,7 +97,7 @@ public interface TlsBucketConfig { /** * The hostname verification algorithm to use in case the server's identity should be checked. - * Should be {@code HTTPS} (default), {@code LDAPS} or an {@code NONE}. + * Should be {@code HTTPS} (default), {@code LDAPS} or {@code NONE}. *

* If set to {@code NONE}, it does not verify the hostname. *

From 1b2e4318a955bb2c82ccc3959610f1051582dc0b Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Thu, 21 Nov 2024 18:46:45 +0200 Subject: [PATCH 32/38] Register jakarta.json.spi.JsonProvider service provider Register jakarta.json.spi.JsonProvider as a service provider so that both the service file and the implementations are included. Co-authored-by: Guillaume Smet (cherry picked from commit 8164c7f35ff3678486189155b9bd5139800d070c) --- .../quarkus/jsonp/deployment/JsonpProcessor.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/extensions/jsonp/deployment/src/main/java/io/quarkus/jsonp/deployment/JsonpProcessor.java b/extensions/jsonp/deployment/src/main/java/io/quarkus/jsonp/deployment/JsonpProcessor.java index 8bc6e02386237..1f803f68dc4dd 100644 --- a/extensions/jsonp/deployment/src/main/java/io/quarkus/jsonp/deployment/JsonpProcessor.java +++ b/extensions/jsonp/deployment/src/main/java/io/quarkus/jsonp/deployment/JsonpProcessor.java @@ -1,20 +1,17 @@ package io.quarkus.jsonp.deployment; -import org.eclipse.parsson.JsonProviderImpl; +import jakarta.json.spi.JsonProvider; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; public class JsonpProcessor { @BuildStep - void build(BuildProducer feature, - BuildProducer reflectiveClass, - BuildProducer resourceBundle) { - reflectiveClass.produce( - ReflectiveClassBuildItem.builder(JsonProviderImpl.class.getName()).build()); + void build(BuildProducer serviceProviders) { + + serviceProviders.produce(ServiceProviderBuildItem.allProvidersFromClassPath(JsonProvider.class.getName())); } + } From 6c57eb6715a2a5597714c0b737788b796c161035 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 22 Nov 2024 15:53:07 +0100 Subject: [PATCH 33/38] Do not add host:port to Health path in Health UI The Health UI will be served from the exact same interface so we don't need to include the host:port. It actually causes issues when you access the Health UI through a proxy as it might point to 0.0.0.0:9000 which will resolve to localhost. Fixes #35980 (cherry picked from commit 4dbf1a8c621f67353dcbf1ff074f225bce1048d4) --- .../smallrye/health/deployment/SmallRyeHealthProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 7c969b1299184..f9e5f130b6a39 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -391,7 +391,7 @@ void registerUiExtension( } String healthPath = nonApplicationRootPathBuildItem.resolveManagementPath(healthConfig.rootPath, - managementInterfaceBuildTimeConfig, launchModeBuildItem); + managementInterfaceBuildTimeConfig, launchModeBuildItem, false); webJarBuildProducer.produce( WebJarBuildItem.builder().artifactKey(HEALTH_UI_WEBJAR_ARTIFACT_KEY) // From 1099725be665e1322c3f6b42d94d43f4578abf4f Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 26 Nov 2024 23:23:03 +0000 Subject: [PATCH 34/38] Temporarily disable OIDC wiremock tests using expired certificates (cherry picked from commit 7fab958802bf42e591eff26ab0972f1d8b7eda2f) --- .../quarkus/it/keycloak/BearerTokenAuthorizationTest.java | 6 ++++++ .../io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java | 2 ++ 2 files changed, 8 insertions(+) diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 1f6f6ee9d13f0..d6f5bb2efbd48 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -22,6 +22,7 @@ import org.awaitility.Awaitility; import org.hamcrest.Matchers; import org.jose4j.jwx.HeaderParameterNames; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.WireMockServer; @@ -61,6 +62,7 @@ public void testSecureAccessSuccessPreferredUsername() { } @Test + @Disabled public void testAccessResourceAzure() throws Exception { String azureToken = readFile("token.txt"); String azureJwk = readFile("jwks.json"); @@ -190,6 +192,7 @@ public void testAccessAdminResourceWithWrongCertS256Thumbprint() { } @Test + @Disabled public void testCertChainWithCustomValidator() throws Exception { X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); @@ -239,6 +242,7 @@ public void testCertChainWithCustomValidator() throws Exception { } @Test + @Disabled public void testAccessAdminResourceWithFullCertChain() throws Exception { X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); @@ -301,6 +305,7 @@ public void testAccessAdminResourceWithFullCertChain() throws Exception { } @Test + @Disabled public void testFullCertChainWithOnlyRootInTruststore() throws Exception { X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); @@ -353,6 +358,7 @@ public void testFullCertChainWithOnlyRootInTruststore() throws Exception { } @Test + @Disabled public void testAccessAdminResourceWithKidOrChain() throws Exception { // token with a matching kid, not x5c String token = Jwt.preferredUserName("admin") diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 0741e298c5236..d5845341e9648 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -50,6 +50,7 @@ import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.WireMockServer; @@ -336,6 +337,7 @@ public void testCodeFlowUserInfo() throws Exception { } @Test + @Disabled public void testCodeFlowUserInfoCachedInIdToken() throws Exception { // Internal ID token, allow in memory cache = false, cacheUserInfoInIdtoken = true final String refreshJwtToken = generateAlreadyExpiredRefreshToken(); From 8d6844e0528e8e4011a7cef578fc4247b6b21216 Mon Sep 17 00:00:00 2001 From: HerrDerb Date: Wed, 27 Nov 2024 09:25:33 +0100 Subject: [PATCH 35/38] Add documentation for @WithTestResource to getting started testing (cherry picked from commit 30f019b206c419d4d5656d9647f244d2dbac6e8d) --- .../asciidoc/getting-started-testing.adoc | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index ab8ff2b88e4dc..6a13199331b05 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1236,7 +1236,30 @@ public @interface WithRepeatableTestResource { } ---- - +=== Usage of `@WithTestResources` + +While test resources provided by `@QuarkusTestResource` are available either globally or restricted to the annotated test class (`restrictToAnnotatedClass`), the annotation `@WithTestResources` allows to additionally group tests by test resources for execution. +`@WithTestResources` has a `scope` property that takes a `TestResourceScope` enum value: + +- `TestResourceScope.MATCHING_RESOURCES` (default) + +Quarkus will group tests with the same test resources and run them together. After a group has been executed, all test resources will be stopped, and the next group will be executed. +- `TestResourceScope.RESTRICTED_TO_CLASS` + +The test resource is available only for the annotated test class and will be stopped after the test class has been executed. This is equivalent to using @QuarkusTestResource with restrictToAnnotatedClass = true. +- `TestResourceScope.GLOBAL` + +The test resource is available globally. +This is equivalent to using `@QuarkusTestResource` with `restrictToAnnotatedClass = false`. + +NOTE: `@QuarkusTestResource` is merely a convenient extension of `@WithTestResources` for the use of global test resources + +BELOW TO EDIT: + +I don't know the behaviour when different scopes are mixed 🤷‍♂️Example: +```java +@WithTestResources(value = TestResourceA.class, scope = TestResourceScope.MATCHING_RESOURCES) +@WithTestResources(value = TestResourceB.class, scope = TestResourceScope.RESTRICTED_TO_CLASS) +@WithTestResources(value = TestResourceC.class, scope = TestResourceScope.GLOBAL) +class TestClass(){} +``` +Also maybe add a use case example for why this annotation got introduced and what for the different scopes are useful, as we currently only use GLOBAL because of time reason. == Hang Detection From 26126633501b0bc3f62436c891ce96a640a01b38 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 27 Nov 2024 11:59:57 +0200 Subject: [PATCH 36/38] Address leftovers in testing docs from #44685 (cherry picked from commit 107e4ea658ec01d911f63f73ddc5bc78e81872c7) --- .../asciidoc/getting-started-testing.adoc | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 6a13199331b05..916b16f57fd08 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1241,25 +1241,15 @@ public @interface WithRepeatableTestResource { While test resources provided by `@QuarkusTestResource` are available either globally or restricted to the annotated test class (`restrictToAnnotatedClass`), the annotation `@WithTestResources` allows to additionally group tests by test resources for execution. `@WithTestResources` has a `scope` property that takes a `TestResourceScope` enum value: -- `TestResourceScope.MATCHING_RESOURCES` (default) + -Quarkus will group tests with the same test resources and run them together. After a group has been executed, all test resources will be stopped, and the next group will be executed. -- `TestResourceScope.RESTRICTED_TO_CLASS` + -The test resource is available only for the annotated test class and will be stopped after the test class has been executed. This is equivalent to using @QuarkusTestResource with restrictToAnnotatedClass = true. -- `TestResourceScope.GLOBAL` + -The test resource is available globally. -This is equivalent to using `@QuarkusTestResource` with `restrictToAnnotatedClass = false`. - -NOTE: `@QuarkusTestResource` is merely a convenient extension of `@WithTestResources` for the use of global test resources - -BELOW TO EDIT: + -I don't know the behaviour when different scopes are mixed 🤷‍♂️Example: -```java -@WithTestResources(value = TestResourceA.class, scope = TestResourceScope.MATCHING_RESOURCES) -@WithTestResources(value = TestResourceB.class, scope = TestResourceScope.RESTRICTED_TO_CLASS) -@WithTestResources(value = TestResourceC.class, scope = TestResourceScope.GLOBAL) -class TestClass(){} -``` -Also maybe add a use case example for why this annotation got introduced and what for the different scopes are useful, as we currently only use GLOBAL because of time reason. +- `TestResourceScope.MATCHING_RESOURCES` (default): Quarkus will group tests with the same test resources and run them together. After a group has been executed, all test resources will be stopped, and the next group will be executed. +- `TestResourceScope.RESTRICTED_TO_CLASS`: The test resource is available only for the annotated test class and will be stopped after the test class has been executed. +- `TestResourceScope.GLOBAL`: Test resources apply to all tests in the testsuite + +Quarkus needs to restart if one of the following is true: + +- At least one the existing test resources is restricted to the test class +- At least one the next test resources is restricted to the test class +- Different {@code MATCHING_RESOURCE} scoped test resources are being used == Hang Detection From 0de52088409474444345b8bd473abf6f98d26ae3 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Tue, 26 Nov 2024 22:20:02 +0100 Subject: [PATCH 37/38] Fix retry count in remote dev mode error message Signed-off-by: Harald Albers (cherry picked from commit 827dd96a6f7eaa5b1ce9a254243390d73528f3d5) --- .../vertx/http/deployment/devmode/HttpRemoteDevClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/HttpRemoteDevClient.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/HttpRemoteDevClient.java index 4952bcfc2a9c8..ea6be6445c424 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/HttpRemoteDevClient.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/HttpRemoteDevClient.java @@ -259,7 +259,7 @@ public void run() { errorCount++; log.error("Remote dev request failed", e); if (errorCount == retryMaxAttempts) { - log.error("Connection failed after 10 retries, exiting"); + log.errorf("Connection failed after %d retries, exiting", errorCount); return; } try { From 1261e269267545bdac67932782cf60ed00cfa51a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:21:18 +0000 Subject: [PATCH 38/38] Bump org.jboss.logmanager:jboss-logmanager Bumps [org.jboss.logmanager:jboss-logmanager](https://github.com/jboss-logging/jboss-logmanager) from 3.0.6.Final to 3.1.0.Final. - [Release notes](https://github.com/jboss-logging/jboss-logmanager/releases) - [Commits](https://github.com/jboss-logging/jboss-logmanager/compare/3.0.6.Final...v3.1.0.Final) --- updated-dependencies: - dependency-name: org.jboss.logmanager:jboss-logmanager dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- independent-projects/bootstrap/pom.xml | 2 +- independent-projects/resteasy-reactive/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index d372873e13037..0009f0d257a07 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -157,7 +157,7 @@ 4.1.4 3.2.0 4.2.2 - 3.0.6.Final + 3.1.0.Final 10.21.0 3.0.4 diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index ee7c24c346442..54bb16c44c307 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -61,7 +61,7 @@ 1.0.1 2.8 1.2.6 - 3.0.6.Final + 3.1.0.Final 1.1.0.Final 2.0.6 23.1.0 diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index e485b49589cae..bd2df0d7761d2 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -51,7 +51,7 @@ 3.9.9 3.26.3 3.6.1.Final - 3.0.6.Final + 3.1.0.Final 3.0.0 1.8.0 3.1.0