diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 983b47e2ab8bae..293c230e2cd9cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,10 +9,6 @@ updates: open-pull-requests-limit: 6 labels: - area/dependencies - groups: - patches: - update-types: - - "patch" allow: - dependency-name: org.jboss:jboss-parent - dependency-name: org.jboss.resteasy:* diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 47fa67d932ce2b..7b39d3b83b18f8 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -398,6 +398,12 @@ jobs: uses: gradle/github-actions/maven-build-scan/save@v1-beta with: job-name: "JVM Tests - JDK ${{matrix.java.name}}" + - name: Upload quarkus-ide-launcher jar + uses: actions/upload-artifact@v3 + with: + name: "quarkus-ide-launcher-999-SNAPSHOT.jar - JDK ${{matrix.java.name}}" + path: | + core/launcher/target/quarkus-ide-launcher-999-SNAPSHOT.jar maven-tests: name: Maven Tests - JDK ${{matrix.java.name}} diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index b269a77a6c5603..a285e66a8445be 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -22,6 +22,7 @@ jobs: run: | echo "preapproved-developpers<> $GITHUB_OUTPUT cat .github/develocity-preapproved-developers.json >> $GITHUB_OUTPUT + echo >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Publish Maven Build Scans uses: gradle/github-actions/maven-build-scan/publish@v1-beta @@ -31,6 +32,7 @@ jobs: develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} skip-comment: true - name: Push to summary + if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} run: | echo -n "Pull request: " >> ${GITHUB_STEP_SUMMARY} cat pr-number.out >> ${GITHUB_STEP_SUMMARY} diff --git a/bom/application/pom.xml b/bom/application/pom.xml index a05b6e0257fc22..12ba92c88e127e 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -116,7 +116,7 @@ 2.2.1.Final 2.0.6 2.0.0.Final - 1.5.4.Final-format-001 + 1.7.0.Final 1.0.1.Final 2.2.2.Final 3.5.1.Final diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index c4a367b89fceeb..77ad5a2dc78ebd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -911,8 +911,10 @@ public NativeImageInvokerInfo build() { if (nativeConfig.autoServiceLoaderRegistration()) { addExperimentalVMOption(nativeImageArgs, "-H:+UseServiceLoaderFeature"); - //When enabling, at least print what exactly is being added: - nativeImageArgs.add("-H:+TraceServiceLoaderFeature"); + if (graalVMVersion.compareTo(GraalVM.Version.VERSION_23_1_0) < 0) { + // When enabling, at least print what exactly is being added. Only possible in <23.1.0 + nativeImageArgs.add("-H:+TraceServiceLoaderFeature"); + } } else { addExperimentalVMOption(nativeImageArgs, "-H:-UseServiceLoaderFeature"); } diff --git a/docs/src/main/asciidoc/amqp-dev-services.adoc b/docs/src/main/asciidoc/amqp-dev-services.adoc index 2c1bb488b362e1..f68fb219e3abce 100644 --- a/docs/src/main/asciidoc/amqp-dev-services.adoc +++ b/docs/src/main/asciidoc/amqp-dev-services.adoc @@ -8,7 +8,7 @@ include::_attributes.adoc[] :categories: messaging :summary: Start AMQP automatically in dev and test modes. :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-amqp -:topics: messaging,amqp,devservices,tooling,testing,devmode +:topics: messaging,amqp,dev-services,testing,dev-mode Dev Services for AMQP automatically starts an AMQP 1.0 broker in dev mode and when running tests. So, you don't have to start a broker manually. diff --git a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc index 0eb728d1d844a5..fc4f80b948176f 100644 --- a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc +++ b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Start Apicurio Registry automatically in dev and test modes. -:topics: messaging,kafka,apicurio,registry,devservices,tooling,testing,devmode +:topics: messaging,kafka,apicurio,registry,dev-services,dev-mode,testing :extensions: io.quarkus:quarkus-apicurio-registry-avro,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If an extension for schema registry, such as `quarkus-apicurio-registry-avro` or `quarkus-confluent-registry-avro`, is present, Dev Services for Apicurio Registry automatically starts an Apicurio Registry instance in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/appcds.adoc b/docs/src/main/asciidoc/appcds.adoc index 543d7da0c8a7d4..59a82eaa0e8c5c 100644 --- a/docs/src/main/asciidoc/appcds.adoc +++ b/docs/src/main/asciidoc/appcds.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core, cloud :summary: This reference guide explains how to enable AppCDS with Quarkus. :topics: appcds,serverless -:extensions: io.quarkus:quarkus-core This reference guide explains how to enable Application Class Data Sharing in your Quarkus applications. diff --git a/docs/src/main/asciidoc/build-analytics.adoc b/docs/src/main/asciidoc/build-analytics.adoc index 820c2d34f868b7..ff94a46c57488e 100644 --- a/docs/src/main/asciidoc/build-analytics.adoc +++ b/docs/src/main/asciidoc/build-analytics.adoc @@ -6,7 +6,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Build analytics :categories: analytics :summary: This guide presents what build analytics is and how to configure it. -:extensions: io.quarkus:quarkus-core The Quarkus team has limited knowledge, from Maven download numbers, of the remarkable growth of Quarkus and the number of users reporting issues/concerns. Still, we need more insight into the platforms, operating system, Java combinations, and build tools our users employ. The build analytics tool aims to provide us with this information. diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 4353b5fbff90ea..96e4e1640cdb01 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: getting-started, native :summary: Build native executables with GraalVM or Mandrel. :topics: native,graalvm,mandrel -:extensions: io.quarkus:quarkus-core This guide covers: diff --git a/docs/src/main/asciidoc/capabilities.adoc b/docs/src/main/asciidoc/capabilities.adoc index 6ef79dbf6a48fe..149fe8e36bfe77 100644 --- a/docs/src/main/asciidoc/capabilities.adoc +++ b/docs/src/main/asciidoc/capabilities.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: How capabilities are implemented and used in Quarkus. :topics: extensions -:extensions: io.quarkus:quarkus-core Quarkus extensions may provide certain capabilities and require certain capabilities to be provided by other extensions in an application to function properly. diff --git a/docs/src/main/asciidoc/class-loading-reference.adoc b/docs/src/main/asciidoc/class-loading-reference.adoc index 65cd0c98e58289..6b740d078c24c0 100644 --- a/docs/src/main/asciidoc/class-loading-reference.adoc +++ b/docs/src/main/asciidoc/class-loading-reference.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: architecture :summary: Learn more about Quarkus class loading infrastructure. :topics: internals,extensions -:extensions: io.quarkus:quarkus-core This document explains the Quarkus class loading architecture. It is intended for extension authors and advanced users who want to understand exactly how Quarkus works. diff --git a/docs/src/main/asciidoc/command-mode-reference.adoc b/docs/src/main/asciidoc/command-mode-reference.adoc index 7fc9b2a7ecdd8e..c4aeb11dbdaa9c 100644 --- a/docs/src/main/asciidoc/command-mode-reference.adoc +++ b/docs/src/main/asciidoc/command-mode-reference.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core, command-line :summary: This reference guide explains how to develop command line applications with Quarkus. :topics: command-line,cli -:extensions: io.quarkus:quarkus-core This reference covers how to write applications that run and then exit. diff --git a/docs/src/main/asciidoc/conditional-extension-dependencies.adoc b/docs/src/main/asciidoc/conditional-extension-dependencies.adoc index 1aec001d92ad96..576f4424f484e5 100644 --- a/docs/src/main/asciidoc/conditional-extension-dependencies.adoc +++ b/docs/src/main/asciidoc/conditional-extension-dependencies.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: Trigger the inclusion on additional extensions based on certain conditions. :topics: extensions -:extensions: io.quarkus:quarkus-core Quarkus extension dependencies are usually configured in the same way as any other project dependencies in the project's build file, e.g. the Maven `pom.xml` or the Gradle build scripts. However, there are dependency types that aren't yet supported out-of-the-box by Maven and Gradle. What we refer here to as "conditional dependencies" is one example. diff --git a/docs/src/main/asciidoc/config-extending-support.adoc b/docs/src/main/asciidoc/config-extending-support.adoc index aca5e13344863e..0a4b215fabbfbb 100644 --- a/docs/src/main/asciidoc/config-extending-support.adoc +++ b/docs/src/main/asciidoc/config-extending-support.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core [[custom-config-source]] == Custom `ConfigSource` diff --git a/docs/src/main/asciidoc/config-mappings.adoc b/docs/src/main/asciidoc/config-mappings.adoc index 89d8265828c006..61cb45b4839234 100644 --- a/docs/src/main/asciidoc/config-mappings.adoc +++ b/docs/src/main/asciidoc/config-mappings.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core With config mappings it is possible to group multiple configuration properties in a single interface that share the same prefix. diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 3703e48677817a..e3c22718953a04 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core IMPORTANT: The content of this guide has been revised and split into additional topics. Please check the <> section. diff --git a/docs/src/main/asciidoc/config.adoc b/docs/src/main/asciidoc/config.adoc index 4ef6b3cd9b2624..1ae0e1a14e7257 100644 --- a/docs/src/main/asciidoc/config.adoc +++ b/docs/src/main/asciidoc/config.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core :summary: Hardcoded values in your code is a no go (even if we all did it at some point ;-)). In this guide, we learn how to configure your application. :topics: configuration -:extensions: io.quarkus:quarkus-core IMPORTANT: The content of this guide and been revised and split into additional topics. Please check the <> section. diff --git a/docs/src/main/asciidoc/context-propagation.adoc b/docs/src/main/asciidoc/context-propagation.adoc index deffd04652f2f3..89955b1eddc97f 100644 --- a/docs/src/main/asciidoc/context-propagation.adoc +++ b/docs/src/main/asciidoc/context-propagation.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core :summary: Learn more about how you can pass contextual information with SmallRye Context Propagation. :topics: context-propagation -:extensions: io.quarkus:quarkus-core Traditional blocking code uses link:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html[`ThreadLocal`] variables to store contextual objects in order to avoid diff --git a/docs/src/main/asciidoc/continuous-testing.adoc b/docs/src/main/asciidoc/continuous-testing.adoc index 6be909bc953875..ef74b9c900785a 100644 --- a/docs/src/main/asciidoc/continuous-testing.adoc +++ b/docs/src/main/asciidoc/continuous-testing.adoc @@ -10,8 +10,7 @@ include::_attributes.adoc[] :numbered: :sectnums: :sectnumlevels: 4 -:topics: testing,dev-ui,tooling,devmode -:extensions: io.quarkus:quarkus-core +:topics: testing,dev-ui,tooling,dev-mode Learn how to use continuous testing in your Quarkus Application. diff --git a/docs/src/main/asciidoc/databases-dev-services.adoc b/docs/src/main/asciidoc/databases-dev-services.adoc index f30efd79d9f482..2590c8751cac8c 100644 --- a/docs/src/main/asciidoc/databases-dev-services.adoc +++ b/docs/src/main/asciidoc/databases-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for Databases :categories: data, tooling include::_attributes.adoc[] -:topics: devservices,data,database,datasource,tooling,testing,devmode +:topics: dev-services,data,database,datasource,dev-mode,testing :extensions: io.quarkus:quarkus-agroal,io.quarkus:quarkus-reactive-mysql-client,io.quarkus:quarkus-reactive-oracle-client,io.quarkus:quarkus-reactive-pg-client,io.quarkus:quarkus-jdbc-db2,io.quarkus:quarkus-jdbc-derby,io.quarkus:quarkus-jdbc-h2,io.quarkus:quarkus-jdbc-mariadb,io.quarkus:quarkus-jdbc-mssql,io.quarkus:quarkus-jdbc-mysql,io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-postgresql When testing or running in dev mode Quarkus can provide you with a zero-config database out of the box, a feature we refer to as Dev Services. diff --git a/docs/src/main/asciidoc/dev-mode-differences.adoc b/docs/src/main/asciidoc/dev-mode-differences.adoc index ed3312ac24a329..b49ee27d3ca6bc 100644 --- a/docs/src/main/asciidoc/dev-mode-differences.adoc +++ b/docs/src/main/asciidoc/dev-mode-differences.adoc @@ -7,8 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: architecture :summary: How dev mode differs from a production application -:topics: internals,devmode -:extensions: io.quarkus:quarkus-core +:topics: internals,dev-mode This document explains how the dev mode in Quarkus differs from a production application. diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index 6e8517de9f7c42..39c49c6c55d391 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: core :summary: A list of all extensions that support Dev Services and their configuration options. -:topics: devservices,tooling,testing,devmode +:topics: dev-services,dev-mode,testing Quarkus supports the automatic provisioning of unconfigured services in development and test mode. We refer to this capability as Dev Services. From a developer's perspective this means that if you include an extension and don't configure it then diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index 83fc9c33b9871f..38bcbb28de722b 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -7,8 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: writing-extensions :summary: Learn how to get your extension to contribute features to the Dev UI (v2). -:topics: dev-ui,tooling,testing -:extensions: io.quarkus:quarkus-core +:topics: dev-ui,testing [NOTE] .Dev UI v2 diff --git a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc index 2ede49bdb9c1fd..5d66575584a33a 100644 --- a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc +++ b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Elasticsearch automatically in dev and test modes -:topics: data,search,elasticsearch,nosql,devservices,tooling,testing,devmode +:topics: data,search,elasticsearch,nosql,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-elasticsearch-java-client,io.quarkus:quarkus-elasticsearch-rest-client,io.quarkus:quarkus-hibernate-search-orm-elasticsearch If any Elasticsearch-related extension is present (e.g. `quarkus-elasticsearch-rest-client` or `quarkus-hibernate-search-orm-elasticsearch`), diff --git a/docs/src/main/asciidoc/extension-codestart.adoc b/docs/src/main/asciidoc/extension-codestart.adoc index 78d63ff702a734..5f36ce736aa24c 100644 --- a/docs/src/main/asciidoc/extension-codestart.adoc +++ b/docs/src/main/asciidoc/extension-codestart.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: Provide users with initial code for extensions when generating Quarkus applications on code.quarkus.io and all the Quarkus tooling. This guide explains how to create and configure a Codestart for an extension. :topics: extensions,codestarts -:extensions: io.quarkus:quarkus-core This guide explains how to create and configure a Quarkus Codestart for an extension. diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 9b6f7ca09cf6a3..0a0f6f88e01a15 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -7,7 +7,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: writing-extensions :topics: extensions,codestarts -:extensions: io.quarkus:quarkus-core Quarkus extensions are distributed as Maven JAR artifacts that application and other libraries may depend on. When a Quarkus application project is built, tested or edited using the Quarkus dev tools, Quarkus extension JAR artifacts will be identified on the application classpath by the presence of the Quarkus extension metadata files in them. This document describes the purpose of each Quarkus extension metadata file and its content. diff --git a/docs/src/main/asciidoc/getting-started-dev-services.adoc b/docs/src/main/asciidoc/getting-started-dev-services.adoc index 8869b3fd521686..14f62ced451bbc 100644 --- a/docs/src/main/asciidoc/getting-started-dev-services.adoc +++ b/docs/src/main/asciidoc/getting-started-dev-services.adoc @@ -9,7 +9,7 @@ include::_attributes.adoc[] :diataxis-type: tutorial :categories: getting-started, data, core :summary: Discover some of the features that make developing with Quarkus a joyful experience. -:topics: getting-started,devservices +:topics: getting-started,dev-services This tutorial shows you how to create an application which writes to and reads from a database. You will use Dev Services, so you will not actually download, configure, or even start the database yourself. diff --git a/docs/src/main/asciidoc/images/ot-to-otel-1.png b/docs/src/main/asciidoc/images/ot-to-otel-1.png new file mode 100644 index 00000000000000..56bc6307500575 Binary files /dev/null and b/docs/src/main/asciidoc/images/ot-to-otel-1.png differ diff --git a/docs/src/main/asciidoc/images/ot-to-otel-2.png b/docs/src/main/asciidoc/images/ot-to-otel-2.png new file mode 100644 index 00000000000000..c3afba21eb51b7 Binary files /dev/null and b/docs/src/main/asciidoc/images/ot-to-otel-2.png differ diff --git a/docs/src/main/asciidoc/images/ot-to-otel-3.png b/docs/src/main/asciidoc/images/ot-to-otel-3.png new file mode 100644 index 00000000000000..86c79a69282e42 Binary files /dev/null and b/docs/src/main/asciidoc/images/ot-to-otel-3.png differ diff --git a/docs/src/main/asciidoc/infinispan-dev-services.adoc b/docs/src/main/asciidoc/infinispan-dev-services.adoc index c2dfc3140a3ad6..436ab6b5a198ca 100644 --- a/docs/src/main/asciidoc/infinispan-dev-services.adoc +++ b/docs/src/main/asciidoc/infinispan-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Infinispan automatically in dev and test modes. -:topics: devservices,data,infinispan,tooling,testing,devmode +:topics: dev-services,data,infinispan,testing,dev-mode :extensions: io.quarkus:quarkus-infinispan-client Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index b6d32ba55e9972..a94da965a3a21b 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Start Apache Kafka automatically in dev and test modes. -:topics: messaging,kafka,devservices,tooling,testing,devmode +:topics: messaging,kafka,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-kafka-client,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messaging-kafka`), Dev Services for Kafka automatically starts a Kafka broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/kafka-dev-ui.adoc b/docs/src/main/asciidoc/kafka-dev-ui.adoc index 2625df6a7e224b..90cf6335f6961e 100644 --- a/docs/src/main/asciidoc/kafka-dev-ui.adoc +++ b/docs/src/main/asciidoc/kafka-dev-ui.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Dev UI extension for Apache Kafka for development purposes. -:topics: messaging,kafka,dev-ui,devmode +:topics: messaging,kafka,dev-ui,dev-mode :extensions: io.quarkus:quarkus-kafka-client,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messaging-kafka`), diff --git a/docs/src/main/asciidoc/kubernetes-dev-services.adoc b/docs/src/main/asciidoc/kubernetes-dev-services.adoc index bd2e9bfa0e48f3..85f86d1ace39de 100644 --- a/docs/src/main/asciidoc/kubernetes-dev-services.adoc +++ b/docs/src/main/asciidoc/kubernetes-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: cloud :summary: Start a Kubernetes API server automatically in dev and test modes. -:topics: devservices,kubernetes,tooling,testing,devmode +:topics: dev-services,kubernetes,testing,dev-mode :extensions: io.quarkus:quarkus-kubernetes-client Dev Services for Kubernetes automatically starts a Kubernetes API server in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/lifecycle.adoc b/docs/src/main/asciidoc/lifecycle.adoc index 539b59006bb73e..911f10ff02f8b5 100644 --- a/docs/src/main/asciidoc/lifecycle.adoc +++ b/docs/src/main/asciidoc/lifecycle.adoc @@ -9,7 +9,7 @@ include::_attributes.adoc[] :keywords: lifecycle event :summary: You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to be notified when an application stops or starts. :topics: lifecycle,observers -:extensions: io.quarkus:quarkus-core,io.quarkus:quarkus-arc +:extensions: io.quarkus:quarkus-arc You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to: diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index c22add0efb800a..a9329d517f4b10 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -9,7 +9,6 @@ include::_attributes.adoc[] :categories: core,getting-started,observability :diataxis-type: reference :topics: logging,observability -:extensions: io.quarkus:quarkus-core Read about the use of logging API in Quarkus, configuring logging output, and using logging adapters to unify the output from other logging APIs. diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index 899a1ed320a618..d5ca44ee979599 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -13,15 +13,14 @@ include::_attributes.adoc[] This guide explains how your Quarkus application can utilize https://opentelemetry.io/[OpenTelemetry] (OTel) to provide distributed tracing for interactive web applications. -OpenTelemetry Metrics and Logging are not yet supported. - [NOTE] ==== +- OpenTelemetry Metrics and Logging are not yet supported. - Quarkus now supports the OpenTelemetry Autoconfiguration for Traces. The configurations match what you can see at https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md[OpenTelemetry SDK Autoconfigure] with the `quarkus.*` prefix. - - Extensions and the libraries they provide, are directly instrumented in Quarkus. The *use of the https://opentelemetry.io/docs/instrumentation/java/automatic/[OpenTelemetry Agent] is not needed nor recommended* due to context propagation issues between imperative and reactive libraries. +- If you come from the legacy OpenTracing extension, there is a xref:telemetry-opentracing-to-otel-tutorial.adoc[guide to help with the migration]. ==== == Prerequisites @@ -138,6 +137,33 @@ All configurations have been updated from `quarkus.opentelemetry.\*` -> `quarkus The legacy configurations are now deprecated but will still work during a transition period. ==== +=== Disable all or parts of the OpenTelemetry extension + +Once you add the dependency, the extension will be enabled by default but there are a few ways to disable the OpenTelemetry extension globally or partially. + +|=== +|Property name |Default value |Description + +|`quarkus.otel.enabled` +|true +|If false, disable the OpenTelemetry usage at *build* time. + +|`quarkus.otel.sdk.disabled` +|false +|Comes from the OpenTelemetry autoconfiguration. If true, will disable the OpenTelemetry SDK usage at *runtime*. + +|`quarkus.otel.traces.enabled` +|true +|If false, disable the OpenTelemetry tracing usage at *build* time. + +|`quarkus.otel.exporter.otlp.enabled` +|true +|If false will disable the default OTLP exporter at *build* time. +|=== + +If you need to enable or disable the exporter at runtime, you can use the <> because it has the ability to filter out all the spans if needed. + + == Run the application The first step is to configure and start the https://opentelemetry.io/docs/collector/[OpenTelemetry Collector] to receive, process and export telemetry data to https://www.jaegertracing.io/[Jaeger] that will display the captured traces. @@ -359,6 +385,7 @@ public class CustomConfiguration { By setting `quarkus.otel.traces.eusp.enabled=true` you can add information about the user related to each span. The user's ID and roles will be added to the span attributes, if available. +[[sampler]] === Sampler A https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling[sampler] decides whether a trace should be discarded or forwarded, effectively managing noise and reducing overhead by limiting the number of collected traces sent to the collector. diff --git a/docs/src/main/asciidoc/opentracing.adoc b/docs/src/main/asciidoc/opentracing.adoc index 88fb99d143a77e..c089dc6847c940 100644 --- a/docs/src/main/asciidoc/opentracing.adoc +++ b/docs/src/main/asciidoc/opentracing.adoc @@ -16,7 +16,7 @@ interactive web applications. [IMPORTANT] ==== -xref:opentelemetry.adoc[OpenTelemetry] is the recommended approach to tracing and telemetry for Quarkus. +xref:opentelemetry.adoc[OpenTelemetry] is the recommended approach to tracing and telemetry for Quarkus and xref:telemetry-opentracing-to-otel-tutorial.adoc[a guide to help with the migration] is available. When Quarkus will upgrade to Eclipse MicroProfile 6, the SmallRye OpenTracing support will be discontinued. ==== diff --git a/docs/src/main/asciidoc/pulsar-dev-services.adoc b/docs/src/main/asciidoc/pulsar-dev-services.adoc index 302f470f37749d..f1f8743bceadb4 100644 --- a/docs/src/main/asciidoc/pulsar-dev-services.adoc +++ b/docs/src/main/asciidoc/pulsar-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for Pulsar include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,pulsar,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,pulsar,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-pulsar With Quarkus Smallrye Reactive Messaging Pulsar extension (`quarkus-smallrye-reactive-messaging-pulsar`) diff --git a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc index c99349f7dd1e55..cf416e7ba075c7 100644 --- a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc +++ b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for RabbitMQ include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq Dev Services for RabbitMQ automatically starts a RabbitMQ broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/rabbitmq-reference.adoc b/docs/src/main/asciidoc/rabbitmq-reference.adoc index 586e338c046fc6..bd421a5b2b202a 100644 --- a/docs/src/main/asciidoc/rabbitmq-reference.adoc +++ b/docs/src/main/asciidoc/rabbitmq-reference.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :extension-status: preview :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq This guide is the companion from the xref:rabbitmq.adoc[Getting Started with RabbitMQ]. diff --git a/docs/src/main/asciidoc/rabbitmq.adoc b/docs/src/main/asciidoc/rabbitmq.adoc index 962edb7171c988..255969990afcf7 100644 --- a/docs/src/main/asciidoc/rabbitmq.adoc +++ b/docs/src/main/asciidoc/rabbitmq.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc :extension-status: preview include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq This guide demonstrates how your Quarkus application can utilize SmallRye Reactive Messaging to interact with RabbitMQ. diff --git a/docs/src/main/asciidoc/redis-dev-services.adoc b/docs/src/main/asciidoc/redis-dev-services.adoc index 0572c08e4343ae..737f4e0e0d8bbe 100644 --- a/docs/src/main/asciidoc/redis-dev-services.adoc +++ b/docs/src/main/asciidoc/redis-dev-services.adoc @@ -8,7 +8,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Redis automatically in dev and test modes. -:topics: data,redis,nosql,devservices,tooling,testing,devmode +:topics: data,redis,nosql,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-redis-client Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc index 86eb5742cf2008..30ef22a2b7993b 100644 --- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc @@ -155,6 +155,12 @@ public class ReactiveResource { The same is true for your third-party libraries. If they happen to depend on servlets you need to find a migration path for them. +=== Log authentication and authorization failures + +The RESTEasy Reactive endpoint security checks are performed before xref:cdi.adoc#interceptors[CDI interceptors] are invoked. +The safest approach to log Quarkus Security authentication exceptions is to ensure that proactive authentication is enabled and to use Vert.x HTTP route failure handlers. +For more information, see the xref:security-proactive-authentication.adoc#customize-auth-exception-responses[Customize authentication exception responses] section of the Proactive authentication guide. + == Client The Reactive REST Client (`quarkus-rest-client-reactive` and its dependencies) replace the legacy `quarkus-rest-client` but leverage Quarkus' build time processing diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index b2214898aa36b6..7d9792a76c145a 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -2219,6 +2219,8 @@ class Filters { } ---- +Such a response filter will also be called for <> exceptions. + Your filters may declare any of the following parameter types: .Filter parameters @@ -2235,7 +2237,7 @@ Your filters may declare any of the following parameter types: |A context object to access the current response |link:{jdkapi}/java/lang/Throwable.html[`Throwable`] -|Any thrown exception, or `null` (only for response filters) +|Any thrown and <> exception, or `null` (only for response filters). |=== @@ -2316,6 +2318,11 @@ Now, whenever a REST method is invoked, the request will be logged into the cons 2019-06-05 12:51:04,485 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1 ---- +[NOTE] +==== +A `ContainerResponseFilter` will also be called for <> exceptions. +==== + === Readers and Writers: mapping entities and HTTP bodies [[readers-writers]] diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 17dcb5a8911128..692575db26ef7f 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -16,7 +16,7 @@ This reference guide explains how to use: The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services. -Please also see xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]. +Also see xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]. == OidcClient @@ -47,7 +47,7 @@ quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus `OidcClient` will discover that the token endpoint URL is `http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens`. -Alternatively, if the discovery endpoint is not available or you would like to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, for example: +Alternatively, if the discovery endpoint is not available or you want to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, for example: [source, properties] ---- @@ -64,7 +64,7 @@ A more compact way to configure the token endpoint URL without the discovery is quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens ---- -Setting 'quarkus.oidc-client.auth-server-url' and 'quarkus.oidc-client.discovery-enabled' is not required in this case. +Setting `quarkus.oidc-client.auth-server-url` and `quarkus.oidc-client.discovery-enabled` is not required in this case. === Supported Token Grants @@ -111,7 +111,7 @@ It can be further customized using a `quarkus.oidc-client.grant-options.password ==== Other Grants -`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which can not be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`. +`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which cannot be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`. Using the `refresh_token` grant which uses an out-of-band refresh token to acquire a new set of tokens will be required if the existing refresh token has been posted to the current Quarkus endpoint for it to acquire the access token. In this case `OidcClient` needs to be configured as follows: @@ -125,7 +125,7 @@ quarkus.oidc-client.grant.type=refresh and then you can use `OidcClient.refreshTokens` method with a provided refresh token to get the access token. -Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants may be required if you are building a complex microservices application and would like to avoid the same `Bearer` token be propagated to and used by more than one service. Please see <> and <> for more details. +Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants might be required if you are building a complex microservices application and want to avoid the same `Bearer` token be propagated to and used by more than one service. See <> and <> for more details. Using `OidcClient` to support the `authorization code` grant might be required if for some reason you cannot use the xref:security-oidc-code-flow-authentication.adoc[Quarkus OIDC extension] to support Authorization Code Flow. If there is a very good reason for you to implement Authorization Code Flow then you can configure `OidcClient` as follows: @@ -153,7 +153,7 @@ and then you can use `OidcClient.accessTokens` method accepting a Map of extra p ==== Grant scopes -You may need to request that a specific set of scopes is associated with an issued access token. +You might need to request that a specific set of scopes is associated with an issued access token. Use a dedicated `quarkus.oidc-client.scopes` list property, for example: `quarkus.oidc-client.scopes=email,phone` === Use OidcClient directly @@ -215,7 +215,7 @@ public class OidcClientResource { @GET public String getResponse() { - // Get the access token which may have been refreshed. + // Get the access token, which might have been refreshed. String accessToken = tokens.getAccessToken(); // Use the access token to configure MP RestClient Authorization header/etc } @@ -395,7 +395,7 @@ It works similarly to the way `OidcClientRequestFilter` does (see < ---- -Write Wiremock based `QuarkusTestResourceLifecycleManager`, for example: +Write a Wiremock-based `QuarkusTestResourceLifecycleManager`, for example: [source, java] ---- package io.quarkus.it.keycloak; @@ -831,7 +831,7 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl } ---- -Prepare the REST test endpoints, you can have the test frontend endpoint which uses the injected MP REST client with a registered OidcClient filter to invoke on the downstream endpoint which echoes the token back, for example, see the `integration-tests/oidc-client-wiremock` in the `main` Quarkus repository. +Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered OidcClient filter, call the downstream endpoint. This endpoint echoes the token back. For example, see the `integration-tests/oidc-client-wiremock` in the `main` Quarkus repository. Set `application.properties`, for example: @@ -856,7 +856,7 @@ If you work with Keycloak then you can use the same approach as described in the === How to check the errors in the logs -Please enable `io.quarkus.oidc.client.runtime.OidcClientImpl` `TRACE` level logging to see more details about the token acquisition and refresh errors: +Enable `io.quarkus.oidc.client.runtime.OidcClientImpl` `TRACE` level logging to see more details about the token acquisition and refresh errors: [source, properties] ---- @@ -864,7 +864,7 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE ---- -Please enable `io.quarkus.oidc.client.runtime.OidcClientRecorder` `TRACE` level logging to see more details about the OidcClient initialization errors: +Enable `io.quarkus.oidc.client.runtime.OidcClientRecorder` `TRACE` level logging to see more details about the OidcClient initialization errors: [source, properties] ---- @@ -967,7 +967,7 @@ quarkus.oidc-token-propagation.exchange-token=true Note `AccessTokenRequestReactiveFilter` will use `OidcClient` to exchange the current token, and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenID Connect Provider. -If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exhange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: +If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: [source,properties] ---- @@ -995,7 +995,7 @@ When you need to propagate the current Authorization Code Flow access token then However, the direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims. -Additionally, a complex application may need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or a completely different set of scopes to access `Service B`. +Additionally, a complex application might need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or a completely different set of scopes to access `Service B`. The following sections show how `AccessTokenRequestFilter` and `JsonWebTokenRequestFilter` can help. @@ -1054,7 +1054,7 @@ quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange quarkus.oidc-token-propagation.exchange-token=true ---- -If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exhange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this: +If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this: [source,properties] ---- @@ -1131,13 +1131,13 @@ smallrye.jwt.new-token.audience=http://downstream-resource smallrye.jwt.new-token.override-matching-claims=true ---- -As already noted above, please use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. +As already noted above, use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. [[integration-testing-token-propagation]] === Testing You can generate the tokens as described in xref:security-oidc-bearer-token-authentication.adoc#integration-testing[OpenID Connect Bearer Token Integration testing] section. -Prepare the REST test endpoints, you can have the test frontend endpoint which uses the injected MP REST client with a registered token propagation filter to invoke on the downstream endpoint, for example, see the `integration-tests/oidc-token-propagation` in the `main` Quarkus repository. +Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered token propagation filter, call the downstream endpoint. For example, see the `integration-tests/oidc-token-propagation` in the `main` Quarkus repository. [[reactive-token-propagation]] == Token Propagation Reactive @@ -1155,7 +1155,7 @@ Add the following Maven Dependency: The `quarkus-oidc-token-propagation-reactive` extension provides `io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter` which can be used to propagate the current `Bearer` or `Authorization Code Flow` access tokens. The `quarkus-oidc-token-propagation-reactive` extension (as opposed to the non-reactive `quarkus-oidc-token-propagation` extension) does not currently support the exchanging or resigning the tokens before the propagation. -However, these features may be added in the future. +However, these features might be added in the future. [[oidc-client-graphql-client]] == GraphQL client integration @@ -1186,7 +1186,7 @@ quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.method=POS NOTE: If you don't specify the `quarkus.oidc-client-graphql.client-name` property, GraphQL clients will use the default OIDC client (without an explicit name). -Specifically for typesafe GraphQL clients, you can override this on a +Specifically for type-safe GraphQL clients, you can override this on a per-client basis by annotating the `GraphQLClientApi` interface with `@io.quarkus.oidc.client.filter.OidcClientFilter`. For example: diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index c405cabb19516d..3e4f7fc20672ef 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -8,7 +8,7 @@ include::_attributes.adoc[] :categories: security :keywords: sso oidc security keycloak :summary: Start Keycloak or other providers automatically in dev and test modes. -:topics: security,oidc,keycloak,devservices,tooling,testing,devmode +:topics: security,oidc,keycloak,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-oidc This guide covers the Dev Services and UI for OpenID Connect (OIDC) Keycloak provider and explains how to support Dev Services and UI for other OpenID Connect providers. diff --git a/docs/src/main/asciidoc/security-proactive-authentication.adoc b/docs/src/main/asciidoc/security-proactive-authentication.adoc index 0fbc81236a9020..22f50c364a8e23 100644 --- a/docs/src/main/asciidoc/security-proactive-authentication.adoc +++ b/docs/src/main/asciidoc/security-proactive-authentication.adoc @@ -94,6 +94,7 @@ public class HelloService { } ---- +[[customize-auth-exception-responses]] == Customize authentication exception responses You can use Jakarta REST `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: diff --git a/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc new file mode 100644 index 00000000000000..63348b95ea7f13 --- /dev/null +++ b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc @@ -0,0 +1,544 @@ +//// +This tutorial 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 +//// +[id="telemetry-opentracing-to-otel-tutorial"] += Migrate from OpenTracing to OpenTelemetry tracing +:categories: observability +:diataxis-type: tutorial +include::_attributes.adoc[] +:topics: observability,opentracing,opentelemetry,tracing,migration +:extensions: io.quarkus:quarkus-smallrye-opentracing,io.quarkus:quarkus-opentelemetry + +Migrate an application from xref:opentracing.adoc[OpenTracing] to xref:opentelemetry.adoc[OpenTelemetry tracing] in Quarkus 3.x. + +The legacy OpenTracing framework has been deprecated in favor of the new OpenTelemetry tracing framework. We announced the https://quarkus.io/blog/quarkus-observability-roadmap-2023/#opentracing-archived[OpenTracing deprecation on November 2022], and we are dropping the extension from Quarkus core repository and moving it to the Quarkiverse Hub. + +It is now time to migrate your application to OpenTelemetry tracing if you haven’t done it yet. + +If you need to migrate from Quarkus 2.16.x please beware that configuration properties are different and you should check the older Quarkus OpenTelemetry guide version, https://quarkus.io/version/2.16/guides/opentelemetry#configuration-reference[here]. + +== Prerequisites + +include::{includes}/prerequisites.adoc[] + +== Summary + +The demo has 5 parts. Please read the summary and then jump to the section that best fits your use case. + +1 - The *starting point* presents the quickstart app that uses OpenTracing + +2 - The first part is good for anyone performing a *big bang change* of OpenTracing when you don't have any manual instrumentation + +3 - This is the *big bang replacement* of OpenTracing when you have manually instrumented the code. We explain the main differences between OpenTracing and OpenTelemetry + +4 - The last part uses the *OpenTracing shim*. This is useful if you have a large application with manually instrumented code. It can help performing the migration step by step because it allows the use of the legacy OpenTracing API on top of new OpenTelemetry API + +5 - Conclusion and additional resources + +The tasks described below fall into 3 categories: + +* Dependencies +* Configuration +* Code + +[[starting-point]] +== Starting point + +This tutorial is built on top of the `opentracing-quickstart` legacy project. + +=== Generate the legacy project + +Create the legacy project by executing the following command: + +:create-app-artifact-id: opentracing-quickstart +:create-app-extensions: resteasy-reactive,quarkus-smallrye-opentracing +:create-app-code: +include::{includes}/devtools/create-app.adoc[] + +This command generates the Maven structure importing the `smallrye-opentracing` extension, which +includes the OpenTracing support and the default https://www.jaegertracing.io/[Jaeger] tracer. + +=== Check out the existing legacy project + +For convenience there is a project in github with all the steps from the tutorial. You can clone it with the following command: + +[source,bash] +---- +git clone git@github.com:quarkusio/opentracing-quickstart-migration.git +---- + +For convenience, https://github.com/quarkusio/opentracing-quickstart-migration[the repository] containing the app to migrate, includes several branches with commits mimicking the migration steps described in this tutorial. You can check out the `main` branch to start from the beginning. + +=== The application + +The Quarkus project has a single endpoint and the related class looks like this: + +[source,java] +---- +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from RESTEasy Reactive"; + } +} +---- + +There is no OpenTracing specific code in the generated project, but the `smallrye-opentracing` extension is present and enabled by default, and it will automatically instrument the code. + +Let's start the Jaeger-all-in-one Docker image, where we will retrieve and see the captured traces: + +[source,bash] +---- +docker run -e COLLECTOR_OTLP_ENABLED=true -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 4317:4317 -p 4318:4318 -p 14250:14250 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest +---- + +At this point you can run the application with Quarkus dev mode: + +include::{includes}/devtools/dev.adoc[] + +If you call the http://localhost:8080/hello[`/hello` endpoint] the related traces can be retrieved in the Jaeger UI at this address: http://localhost:16686/ + +They will look like this: + +image::ot-to-otel-1.png[alt=OpenTracing span,role="center"] + +== Big bang change from OpenTracing to OpenTelemetry + +This is the happiest path, in this case there is no manual instrumentation. We can do a big bang change from OpenTracing to OpenTelemetry without side effects. + +=== Change dependencies + +To migrate between the two frameworks, you must drop the old `quarkus-smallrye-opentracing` extension and replace it by the `quarkus-opentelemetry` extension in the build file: + +The legacy extension is removed from the project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-opentracing + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-smallrye-opentracing") +---- + +The new one is added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +---- + +=== Application properties + +You should remove the old OpenTracing properties, starting with `quarkus.jaeger.*` from the `application.properties` file, like in this example: + +[source,application.properties] +---- +#Legacy OpenTracing properties to be removed +quarkus.jaeger.service-name=legume +quarkus.jaeger.sampler-type=const +quarkus.jaeger.sampler-param=1 +quarkus.jaeger.endpoint=http://localhost:14268/api/traces +quarkus.jaeger.log-trace-context=true +---- + +If you use the default values in the OpenTelemetry properties, there is no necessity to include anything in the `application.properties` file. + +Some common properties to migrate are: + +|=== +|Legacy OpenTracing property | New OpenTelemetry property + +|`quarkus.jaeger.service-name=legume` +|`quarkus.application.name=legume` + +|`quarkus.jaeger.endpoint=http://localhost:14268/api/traces` +|`quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4317` + +|`quarkus.jaeger.auth-token` +|`quarkus.otel.exporter.otlp.traces.headers` + +|`quarkus.jaeger.sampler-type` +|`quarkus.otel.traces.sampler` + +|`quarkus.jaeger.sampler-param` +|`quarkus.otel.traces.sampler.arg` + +|`quarkus.jaeger.tags` +|`quarkus.otel.resource.attributes` + +|`quarkus.jaeger.propagation` +|`quarkus.otel.propagators` +|=== + +The way the extensions can be enabled and disabled is very different. The OpenTelemetry extension is enabled by default and you can disable all or parts of it by checking xref:opentelemetry.adoc#disable-all-or-parts-of-the-opentelemetry-extension[this section of the OpenTelemetry guide]. + +All the OpenTelemetry properties and their defaults can be found in the xref:opentelemetry.adoc#configuration-reference[OpenTelemetry configuration reference]. + +=== Run the application + +Restarting Quarkus is not needed, auto-reload should have kicked in and you now can call the http://localhost:8080/hello[`/hello` endpoint] and then see the traces in the Jaeger UI: http://localhost:16686/ + +However, you can now see spans produced by the OpenTelemetry's auto-instrumentation instead of the OpenTracing one: + +image::ot-to-otel-2.png[alt=OpenTelemetry span,role="center"] + +If you don't have any manual instrumentation of your own, you are done! + +== The big bang replacement, when you have manual instrumentation + +Let's say instead of the `GreetingResource` class from above, you have something more complex. You will need additional work on top of the changes from the <>. + +This class now uses the `@Traced` annotation and creates a "manual" programmatic span. + +Copy/paste that code for the `GreetingResource` class in the quickstart project: + +[[greeting-resource-starting-point]] +=== The GreetingsResource with OpenTracing manual instrumentation + +[source,java] +---- +package org.acme; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.opentracing.Traced; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentracing.Tracer legacyTracer; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Traced(operationName = "Not needed, will change the current span name") <2> + public String hello() { + // Add a tag to the active span + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); <3> + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); <4> + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +<1> The legacy OpenTracing tracer, must be replaced by the new OpenTelemetry tracer. +<2> The `@Traced` annotation is replaced by the `@WithSpan` annotation but beware that this new annotation will always create a new Span. You shouldn't use it on JAX-RS endpoints because they are already instrumented. +<3> The `Tag` class is replaced by the `Attribute` class. `Tags` is replaced by the `SemanticAttributes` class, which should be used whenever possible, to keep attribute names consistent with the specification. +<4> There are new methods to handle errors in OpenTelemetry. + +The OpenTelemetry tracer is not compatible with the OpenTracing API. The main changes are summarized in the following table: + +|=== +|Note |MicroProfile OpenTracing v3 |OpenTelemetry + +|1 +|`@Inject io.opentracing.Tracer legacyTracer;` +|`@Injectio.opentelemetry.api.trace.Tracer otelTracer;` + +|2 +|`@Traced` +|`@WithSpan` + +|3 +|Tag +|Attribute + +|3 +|Tags +|SemanticAttributes + +|4 +|```innerSpan.setTag("error", true); +innerSpan.setTag("error.message", e.getMessage());``` +|```innerSpan.setStatus(ERROR); +innerSpan.recordException(e);``` + +|- +|Baggage carried by SpanContext in the Span | Baggage is an independent signal propagated in parallel with the OTel Context +|Baggage is an independent signal propagated in parallel with the OTel Context, it's not part of it. +|=== + +Once the dependencies have been updated, the above class will break the build because the quickstart project is now running with OpenTelemetry. Errors like this will show up in the logs: + +[source,bash] +---- +2023-10-27 16:11:12,454 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (main) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors + [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type io.opentracing.Tracer and qualifiers [@Default] +... +---- + +The new OpenTelemetry API must be used instead. This is one way to migrate the code: + +=== GreetingsResource with OpenTelemetry manual instrumentation + +[source,java] +---- +package org.acme; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import static io.opentelemetry.api.trace.StatusCode.*; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentelemetry.api.trace.Tracer otelTracer; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @WithSpan(value = "Not needed, will create a new span, child of the automatic JAX-RS span") + public String hello() { + // Add a tag to the active span + Span incomingSpan = Span.current(); + incomingSpan.setAttribute(SemanticAttributes.CODE_NAMESPACE, "GreetingResource"); + + // Create a manual inner span + Span innerSpan = otelTracer.spanBuilder("Count response chars").startSpan(); + try (Scope scope = innerSpan.makeCurrent()) { + final String response = "Hello from RESTEasy Reactive"; + innerSpan.setAttribute("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setStatus(ERROR); + innerSpan.recordException(e); + throw e; + } finally { + innerSpan.end(); + } + } +} + +---- + +Once you remove all the OpenTracing dependencies the code will build. Don't forget to double check if the traces contain the right spans. You can see them in the Jaeger UI: http://localhost:16686/. + +== The OpenTracing shim + +In this section, we present an OpenTelemetry library that can smooth the transition by providing access to the legacy OpenTracing API. This can help with the migration of large applications with many manual instrumentation points. + +To proceed with this section, the code project must be its <>. If you have changes related to the previous sections, please revert them or re-generate the project according to the <> instructions before proceeding. + +=== The dependencies + +Remove the `quarkus-smallrye-opentracing` extension and add the `quarkus-opentelemetry` extension and the `opentelemetry-opentracing-shim` library to the build file: + +The legacy extension is removed from the project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-opentracing + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-smallrye-opentracing") +---- + +The new one is added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + + + io.opentelemetry + opentelemetry-opentracing-shim + + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +implementation("io.quarkus:opentelemetry-opentracing-shim") +---- + +=== The code changes + +Remembering the initial version of the `GreetingResource` class from the <>: +[source, java] +---- +package org.acme; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.opentracing.Traced; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentracing.Tracer legacyTracer; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Traced(operationName = "Not needed, will change the current span name") <2> + public String hello() { + // Add a tag to the active span + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); <3> + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +<1> The `Tracer` annotation must be removed and instead, we need to inject the OpenTelemetry SDK. We will need it in <3>. +<2> The `@Traced` annotation is replaced by the `@WithSpan` annotation but beware that this new annotation will always create a new Span. You shouldn't use it on JAX-RS endpoints and we only have it here for demonstration purposes. +<3> We must obtain an instance of the `legacyTracer`. The Shim includes a utility class for this purpose: `Tracer legacyTracer = OpenTracingShim.createTracerShim(openTelemetry);` + +After the changes, the code will compile and you will be able to use both the OpenTracing and OpenTelemetry APIs at the same time: + +[source,java] +---- +package org.acme; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentelemetry.api.OpenTelemetry openTelemetry; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @WithSpan(value = "Not needed, will create a new span, child of the automatic JAX-RS span") + public String hello() { + // Add a tag to the active span + Tracer legacyTracer = OpenTracingShim.createTracerShim(openTelemetry); + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +[IMPORTANT] +==== +It's advised not to utilize the shim for a permanent solution but solely as a tool to smooth the migration. +==== + +== Conclusion and additional resources + +This tutorial showed how to migrate an application from OpenTracing to OpenTelemetry tracing in Quarkus 3.x. + +You can find more information about the migration to OpenTelemetry at: + +* https://github.com/quarkusio/opentracing-quickstart-migration[The companion GitHub repository for this tutorial] +* https://opentelemetry.io/docs/migration/opentracing/[Migrating from OpenTracing] +* https://opentelemetry.io/docs/specs/otel/compatibility/opentracing/[OpenTracing compatibility with OpenTelemetry] diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index 732931232ddb66..eba2cde1575509 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -15,7 +15,6 @@ include::_attributes.adoc[] :thread: https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Thread.html :pgsql-driver: https://javadoc.io/doc/org.postgresql/postgresql/latest/index.html :topics: virtual-threads -:extensions: io.quarkus:quarkus-core This guide explains how to benefit from Java 21+ virtual threads in Quarkus application. diff --git a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java index 162c4df7484811..f999e542440c4c 100644 --- a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java +++ b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java @@ -7,11 +7,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -27,6 +29,7 @@ import org.asciidoctor.ast.Document; import org.asciidoctor.ast.StructuralNode; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.exc.StreamWriteException; @@ -55,6 +58,8 @@ public class YamlMetadataGenerator { final static String INCL_ATTRIBUTES = "include::_attributes.adoc[]\n"; final static String YAML_FRONTMATTER = "---\n"; + private static final String COMPATIBILITY_TOPIC = "compatibility"; + public static void main(String[] args) throws Exception { System.out.println("[INFO] Creating YAML metadata generator: " + List.of(args)); YamlMetadataGenerator generator = new YamlMetadataGenerator() @@ -120,6 +125,8 @@ public void writeYamlFiles() throws StreamWriteException, DatabindException, IOE om.writeValue(targetDir.resolve("indexByType.yaml").toFile(), index); om.writeValue(targetDir.resolve("indexByFile.yaml").toFile(), metadata); + om.writeValue(targetDir.resolve("relations.yaml").toFile(), index.relationsByUrl(metadata)); + om.writeValue(targetDir.resolve("errorsByType.yaml").toFile(), messages); om.writeValue(targetDir.resolve("errorsByFile.yaml").toFile(), messages.allByFile()); } @@ -455,6 +462,52 @@ public Map metadataByFile() { .collect(Collectors.toMap(v -> v.filename, v -> v, (o1, o2) -> o1, TreeMap::new)); } + public Map relationsByUrl(Map metadataByFile) { + Map relationsByUrl = new TreeMap<>(); + + for (Entry currentMetadataEntry : metadataByFile.entrySet()) { + DocRelations docRelations = new DocRelations(); + + for (Entry candidateMetadataEntry : metadataByFile.entrySet()) { + if (candidateMetadataEntry.getKey().equals(currentMetadataEntry.getKey())) { + continue; + } + + DocMetadata candidateMetadata = candidateMetadataEntry.getValue(); + int extensionMatches = 0; + for (String extension : currentMetadataEntry.getValue().getExtensions()) { + if (candidateMetadata.getExtensions().contains(extension)) { + extensionMatches++; + } + } + if (extensionMatches > 0) { + docRelations.sameExtensions.add( + new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), + candidateMetadata.getType(), extensionMatches)); + } + + int topicMatches = 0; + for (String topic : currentMetadataEntry.getValue().getTopics()) { + if (candidateMetadata.getTopics().contains(topic)) { + topicMatches++; + } + } + if (topicMatches > 0 && (!candidateMetadata.getTopics().contains(COMPATIBILITY_TOPIC) + || currentMetadataEntry.getValue().getTopics().contains(COMPATIBILITY_TOPIC))) { + docRelations.sameTopics + .add(new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), + candidateMetadata.getType(), topicMatches)); + } + } + + if (!docRelations.isEmpty()) { + relationsByUrl.put(currentMetadataEntry.getValue().getUrl(), docRelations); + } + } + + return relationsByUrl; + } + // convenience public Map messagesByFile() { return messages.allByFile(); @@ -595,6 +648,78 @@ public int compareTo(DocMetadata that) { } } + @JsonInclude(value = Include.NON_EMPTY) + public static class DocRelations { + + final Set sameTopics = new TreeSet<>(DocRelationComparator.INSTANCE); + + final Set sameExtensions = new TreeSet<>(DocRelationComparator.INSTANCE); + + public Set getSameTopics() { + return sameTopics; + } + + public Set getSameExtensions() { + return sameExtensions; + } + + @JsonIgnore + public boolean isEmpty() { + return sameTopics.isEmpty() && sameExtensions.isEmpty(); + } + } + + @JsonInclude(value = Include.NON_EMPTY) + public static class DocRelation { + + final String title; + + final String url; + + final String type; + + final int matches; + + DocRelation(String title, String url, String type, int matches) { + this.title = title; + this.url = url; + this.type = type; + this.matches = matches; + } + + public String getTitle() { + return title; + } + + public String getUrl() { + return url; + } + + public String getType() { + return type; + } + + public int getMatches() { + return matches; + } + } + + public static class DocRelationComparator implements Comparator { + + static final DocRelationComparator INSTANCE = new DocRelationComparator(); + + @Override + public int compare(DocRelation o1, DocRelation o2) { + int compareMatches = o2.matches - o1.matches; + + if (compareMatches != 0) { + return compareMatches; + } + + return o1.title.compareToIgnoreCase(o2.title); + } + } + @JsonInclude(value = Include.NON_EMPTY) public static class FileMessages { Collection errors; diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index 3e323cd748ec01..1041831b124778 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -120,6 +120,15 @@ if [ -f target/indexByType.yaml ]; then echo fi +if [ -f target/relations.yaml ]; then + echo + echo "Copying target/relations.yaml to $TARGET_INDEX/relations.yaml" + mkdir -p $TARGET_INDEX + echo "# Generated file. Do not edit" > $TARGET_INDEX/relations.yaml + cat target/relations.yaml >> $TARGET_INDEX/relations.yaml + echo +fi + echo "Sync done!" echo "==========" diff --git a/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js b/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js index 7e69d75f9976e2..98d40dd857ada0 100644 --- a/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js +++ b/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js @@ -1,7 +1,8 @@ import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { JsonRpc } from 'jsonrpc'; - +import { devuiState } from 'devui-state'; +import { notifier } from 'notifier'; import { Graphviz } from "@hpcc-js/wasm/graphviz.js"; import '@vaadin/details'; @@ -52,7 +53,7 @@ export class QwcKafkaStreamsTopology extends QwcHotReloadElement { Graphviz Mermaid -

${this._tabContent}

`; +

${this._tabContent}

`; } return html` this._downloadTopologyAsPng()}> + Download as PNG + `; } else { this._tabContent = html`Graph engine not started.`; } } + _downloadTopologyAsPng() { + let svgData = this.renderRoot?.querySelector('#svgSpan').getElementsByTagName("svg")[0]; + let img = new Image(svgData.width.baseVal.value, svgData.height.baseVal.value); + img.src = `data:image/svg+xml;base64,${btoa(new XMLSerializer().serializeToString(svgData))}`; + img.onload = function () { + let cnv = document.createElement('canvas'); + cnv.width = img.width; + cnv.height = img.height; + cnv.getContext("2d").drawImage(img, 0, 0); + cnv.toBlob((blob) => { + let lnk = document.createElement('a'); + lnk.href = URL.createObjectURL(blob); + lnk.download = "Topology-" + devuiState.applicationInfo.applicationName + "-" + new Date().toISOString().replace(/\D/g,'') + ".png"; + lnk.click(); + notifier.showSuccessMessage(lnk.download + " downloaded.", 'bottom-end'); + }); + } + } + _selectDetailsTab() { this._tabContent = html` diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java index fc82604f416587..24f25e424e5c2c 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java @@ -66,7 +66,7 @@ public void accept(TopologyParserContext context) { private static final RawTopologyItemParser SOURCE = new RawTopologyItemParser() { private final Pattern sourcePattern = Pattern - .compile("Source:\\s+(?\\S+)\\s+\\(topics:\\s+\\[(?.*)\\]\\).*"); + .compile("Source:\\s+(?\\S+)\\s+\\(topics:\\s+((\\[(?.*)\\])|(?.*)\\)).*"); private Matcher matcher; @Override @@ -77,7 +77,11 @@ public boolean test(String line) { @Override public void accept(TopologyParserContext context) { - context.addSources(matcher.group("source"), matcher.group("topics").split(",")); + if (matcher.group("topics") != null) { + context.addSources(matcher.group("source"), matcher.group("topics").split(",")); + } else if (matcher.group("regex") != null) { + context.addRegexSource(matcher.group("source"), matcher.group("regex")); + } } }; diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java index cdbf1ec7c1a51d..6a047065af724a 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java @@ -41,6 +41,16 @@ void addSources(String source, String[] topics) { }); } + void addRegexSource(String source, String regex) { + currentNode = source; + final var cleanRegex = regex.trim(); + if (!cleanRegex.isEmpty()) { + sources.add(cleanRegex); + graphviz.addRegexSource(source, cleanRegex); + mermaid.addRegexSource(source, cleanRegex); + } + } + void addStores(String[] stores, String processor, boolean join) { currentNode = processor; Arrays.stream(stores) @@ -71,8 +81,8 @@ String toGraph() { final var res = new ArrayList(); res.add("digraph {"); - res.add(" fontname=\"Helvetica\"; fontsize=\"10\";"); - res.add(" node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=\"Helvetica\" fontsize=\"10\"];"); + res.add(" fontname=Helvetica; fontsize=10;"); + res.add(" node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=Helvetica fontsize=10];"); nodes.forEach(n -> res.add(' ' + n + ';')); subGraphs.entrySet().forEach(e -> { res.add(" subgraph cluster" + e.getKey() + " {"); @@ -80,11 +90,6 @@ String toGraph() { e.getValue().forEach(v -> res.add(" " + v + ';')); res.add(" }"); }); - for (int i = 0; i < subGraphs.size(); i++) { - res.add(" subgraph cluster" + i + " {"); - res.add(" label=\"Sub-Topology: " + i + "\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";"); - res.add(" }"); - } edges.forEach(e -> res.add(' ' + e + ';')); res.add("}"); @@ -108,6 +113,15 @@ private void addSource(String source, String topic) { subGraphs.get(currentGraph).add(toId(source)); } + private void addRegexSource(String source, String regex) { + final var regexId = "REGEX_" + nodes.size(); + final var regexLabel = regex.replaceAll("\\\\", "\\\\\\\\"); + nodes.add(regexId + " [label=\"" + regexLabel + "\" shape=invhouse style=dashed margin=\"0,0\"]"); + nodes.add(toId(source) + " [label=\"" + toLabel(source) + "\"]"); + edges.add(regexId + " -> " + toId(source)); + subGraphs.get(currentGraph).add(toId(source)); + } + private void addTarget(String target, String node) { nodes.add(toId(target) + " [label=\"" + toLabel(target) + "\"]"); edges.add(toId(node) + " -> " + toId(target)); @@ -164,6 +178,10 @@ private void addSource(String source, String topic) { endpoints.add(topic + '[' + topic + "] --> " + source + '(' + toName(source) + ')'); } + private void addRegexSource(String source, String regex) { + endpoints.add("REGEX_" + endpoints.size() + '[' + regex + "] --> " + source + '(' + toName(source) + ')'); + } + private void addTarget(String target, String node) { subTopologies.add(' ' + node + '[' + toName(node) + "] --> " + target + '(' + toName(target) + ')'); } diff --git a/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java b/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java index 8c40491616d87e..854ac92d972512 100644 --- a/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java +++ b/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java @@ -31,66 +31,77 @@ public void shouldParsingStayConstant() { + " --> KSTREAM-SINK-0000000007\n" + " <-- KSTREAM-AGGREGATE-0000000005\n" + " Sink: KSTREAM-SINK-0000000007 (topic: temperatures-aggregated)\n" - + " <-- KTABLE-TOSTREAM-0000000006"; + + " <-- KTABLE-TOSTREAM-0000000006\n" + + "\n" + + " Sub-topology: 2\n" + + " Source: KSTREAM-SOURCE-0000000008 (topics: notification\\..+)\n" + + " --> KSTREAM-FOREACH-0000000009\n" + + " Processor: KSTREAM-FOREACH-0000000009 (stores: [])\n" + + " --> none\n" + + " <-- KSTREAM-SOURCE-0000000008"; final var actual = rpcService.parseTopologyDescription(expectedDescribe); assertEquals(expectedDescribe, actual.getString("describe")); - assertEquals("[0, 1]", actual.getString("subTopologies")); - assertEquals("[temperature-values, weather-stations]", actual.getString("sources")); + assertEquals("[0, 1, 2]", actual.getString("subTopologies")); + assertEquals("[notification\\..+, temperature-values, weather-stations]", actual.getString("sources")); assertEquals("[temperatures-aggregated]", actual.getString("sinks")); assertEquals("[weather-stations-STATE-STORE-0000000000, weather-stations-store]", actual.getString("stores")); - assertEquals("digraph {\n" + - " fontname=\"Helvetica\"; fontsize=\"10\";\n" + - " node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=\"Helvetica\" fontsize=\"10\"];\n" + - " weather_stations [label=\"weather\\nstations\" shape=invhouse margin=\"0,0\"];\n" + - " KSTREAM_SOURCE_0000000001 [label=\"KSTREAM\\nSOURCE\\n0000000001\"];\n" + - " KTABLE_SOURCE_0000000002 [label=\"KTABLE\\nSOURCE\\n0000000002\"];\n" + - " weather_stations_STATE_STORE_0000000000 [label=\"weather\\nstations\\nSTATE\\nSTORE\\n0000000000\" shape=cylinder];\n" - + - " temperature_values [label=\"temperature\\nvalues\" shape=invhouse margin=\"0,0\"];\n" + - " KSTREAM_SOURCE_0000000003 [label=\"KSTREAM\\nSOURCE\\n0000000003\"];\n" + - " KSTREAM_LEFTJOIN_0000000004 [label=\"KSTREAM\\nLEFTJOIN\\n0000000004\"];\n" + - " KSTREAM_AGGREGATE_0000000005 [label=\"KSTREAM\\nAGGREGATE\\n0000000005\"];\n" + - " weather_stations_store [label=\"weather\\nstations\\nstore\" shape=cylinder];\n" + - " KTABLE_TOSTREAM_0000000006 [label=\"KTABLE\\nTOSTREAM\\n0000000006\"];\n" + - " KSTREAM_SINK_0000000007 [label=\"KSTREAM\\nSINK\\n0000000007\"];\n" + - " temperatures_aggregated [label=\"temperatures\\naggregated\" shape=house margin=\"0,0\"];\n" + - " subgraph cluster0 {\n" + - " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " KSTREAM_SOURCE_0000000001;\n" + - " KTABLE_SOURCE_0000000002;\n" + - " }\n" + - " subgraph cluster1 {\n" + - " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " KSTREAM_SOURCE_0000000003;\n" + - " KSTREAM_LEFTJOIN_0000000004;\n" + - " KSTREAM_AGGREGATE_0000000005;\n" + - " KTABLE_TOSTREAM_0000000006;\n" + - " KSTREAM_SINK_0000000007;\n" + - " }\n" + - " subgraph cluster0 {\n" + - " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " }\n" + - " subgraph cluster1 {\n" + - " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " }\n" + - " weather_stations -> KSTREAM_SOURCE_0000000001;\n" + - " KSTREAM_SOURCE_0000000001 -> KTABLE_SOURCE_0000000002;\n" + - " KTABLE_SOURCE_0000000002 -> weather_stations_STATE_STORE_0000000000;\n" + - " temperature_values -> KSTREAM_SOURCE_0000000003;\n" + - " KSTREAM_SOURCE_0000000003 -> KSTREAM_LEFTJOIN_0000000004;\n" + - " KSTREAM_LEFTJOIN_0000000004 -> KSTREAM_AGGREGATE_0000000005;\n" + - " KSTREAM_AGGREGATE_0000000005 -> weather_stations_store;\n" + - " KSTREAM_AGGREGATE_0000000005 -> KTABLE_TOSTREAM_0000000006;\n" + - " KTABLE_TOSTREAM_0000000006 -> KSTREAM_SINK_0000000007;\n" + - " KSTREAM_SINK_0000000007 -> temperatures_aggregated;\n" + - "}", actual.getString("graphviz")); + assertEquals("digraph {\n" + + " fontname=Helvetica; fontsize=10;\n" + + " node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=Helvetica fontsize=10];\n" + + " weather_stations [label=\"weather\\nstations\" shape=invhouse margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000001 [label=\"KSTREAM\\nSOURCE\\n0000000001\"];\n" + + " KTABLE_SOURCE_0000000002 [label=\"KTABLE\\nSOURCE\\n0000000002\"];\n" + + " weather_stations_STATE_STORE_0000000000 [label=\"weather\\nstations\\nSTATE\\nSTORE\\n0000000000\" shape=cylinder];\n" + + " temperature_values [label=\"temperature\\nvalues\" shape=invhouse margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000003 [label=\"KSTREAM\\nSOURCE\\n0000000003\"];\n" + + " KSTREAM_LEFTJOIN_0000000004 [label=\"KSTREAM\\nLEFTJOIN\\n0000000004\"];\n" + + " KSTREAM_AGGREGATE_0000000005 [label=\"KSTREAM\\nAGGREGATE\\n0000000005\"];\n" + + " weather_stations_store [label=\"weather\\nstations\\nstore\" shape=cylinder];\n" + + " KTABLE_TOSTREAM_0000000006 [label=\"KTABLE\\nTOSTREAM\\n0000000006\"];\n" + + " KSTREAM_SINK_0000000007 [label=\"KSTREAM\\nSINK\\n0000000007\"];\n" + + " temperatures_aggregated [label=\"temperatures\\naggregated\" shape=house margin=\"0,0\"];\n" + + " REGEX_12 [label=\"notification\\\\..+\" shape=invhouse style=dashed margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000008 [label=\"KSTREAM\\nSOURCE\\n0000000008\"];\n" + + " KSTREAM_FOREACH_0000000009 [label=\"KSTREAM\\nFOREACH\\n0000000009\"];\n" + + " subgraph cluster0 {\n" + + " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000001;\n" + + " KTABLE_SOURCE_0000000002;\n" + + " }\n" + + " subgraph cluster1 {\n" + + " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000003;\n" + + " KSTREAM_LEFTJOIN_0000000004;\n" + + " KSTREAM_AGGREGATE_0000000005;\n" + + " KTABLE_TOSTREAM_0000000006;\n" + + " KSTREAM_SINK_0000000007;\n" + + " }\n" + + " subgraph cluster2 {\n" + + " label=\"Sub-Topology: 2\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000008;\n" + + " KSTREAM_FOREACH_0000000009;\n" + + " }\n" + + " weather_stations -> KSTREAM_SOURCE_0000000001;\n" + + " KSTREAM_SOURCE_0000000001 -> KTABLE_SOURCE_0000000002;\n" + + " KTABLE_SOURCE_0000000002 -> weather_stations_STATE_STORE_0000000000;\n" + + " temperature_values -> KSTREAM_SOURCE_0000000003;\n" + + " KSTREAM_SOURCE_0000000003 -> KSTREAM_LEFTJOIN_0000000004;\n" + + " KSTREAM_LEFTJOIN_0000000004 -> KSTREAM_AGGREGATE_0000000005;\n" + + " KSTREAM_AGGREGATE_0000000005 -> weather_stations_store;\n" + + " KSTREAM_AGGREGATE_0000000005 -> KTABLE_TOSTREAM_0000000006;\n" + + " KTABLE_TOSTREAM_0000000006 -> KSTREAM_SINK_0000000007;\n" + + " KSTREAM_SINK_0000000007 -> temperatures_aggregated;\n" + + " REGEX_12 -> KSTREAM_SOURCE_0000000008;\n" + + " KSTREAM_SOURCE_0000000008 -> KSTREAM_FOREACH_0000000009;\n" + + "}", actual.getString("graphviz")); assertEquals("graph TD\n" + " weather-stations[weather-stations] --> KSTREAM-SOURCE-0000000001(KSTREAM-
SOURCE-
0000000001)\n" + " KTABLE-SOURCE-0000000002[KTABLE-
SOURCE-
0000000002] --> weather-stations-STATE-STORE-0000000000(weather-
stations-
STATE-
STORE-
0000000000)\n" + " temperature-values[temperature-values] --> KSTREAM-SOURCE-0000000003(KSTREAM-
SOURCE-
0000000003)\n" + " KSTREAM-AGGREGATE-0000000005[KSTREAM-
AGGREGATE-
0000000005] --> weather-stations-store(weather-
stations-
store)\n" + " KSTREAM-SINK-0000000007[KSTREAM-
SINK-
0000000007] --> temperatures-aggregated(temperatures-aggregated)\n" + + " REGEX_5[notification\\..+] --> KSTREAM-SOURCE-0000000008(KSTREAM-
SOURCE-
0000000008)\n" + " subgraph Sub-Topology: 0\n" + " KSTREAM-SOURCE-0000000001[KSTREAM-
SOURCE-
0000000001] --> KTABLE-SOURCE-0000000002(KTABLE-
SOURCE-
0000000002)\n" + " end\n" @@ -99,6 +110,9 @@ public void shouldParsingStayConstant() { + " KSTREAM-LEFTJOIN-0000000004[KSTREAM-
LEFTJOIN-
0000000004] --> KSTREAM-AGGREGATE-0000000005(KSTREAM-
AGGREGATE-
0000000005)\n" + " KSTREAM-AGGREGATE-0000000005[KSTREAM-
AGGREGATE-
0000000005] --> KTABLE-TOSTREAM-0000000006(KTABLE-
TOSTREAM-
0000000006)\n" + " KTABLE-TOSTREAM-0000000006[KTABLE-
TOSTREAM-
0000000006] --> KSTREAM-SINK-0000000007(KSTREAM-
SINK-
0000000007)\n" + + " end\n" + + " subgraph Sub-Topology: 2\n" + + " KSTREAM-SOURCE-0000000008[KSTREAM-
SOURCE-
0000000008] --> KSTREAM-FOREACH-0000000009(KSTREAM-
FOREACH-
0000000009)\n" + " end", actual.getString("mermaid")); } } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 46527078111bae..45b1923d5d805c 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -166,13 +166,13 @@ public static enum Method { BASIC, /** - * client_secret_post: client id and secret are submitted as the 'client_id' and 'client_secret' form + * client_secret_post: client id and secret are submitted as the `client_id` and `client_secret` form * parameters. */ POST, /** - * client_secret_jwt: client id and generated JWT secret are submitted as the 'client_id' and 'client_secret' + * client_secret_jwt: client id and generated JWT secret are submitted as the `client_id` and `client_secret` * form * parameters. */ @@ -223,7 +223,7 @@ public void setSecretProvider(Provider secretProvider) { } /** - * Supports the client authentication 'client_secret_jwt' and 'private_key_jwt' methods which involve sending a JWT + * Supports the client authentication 'client_secret_jwt' and `private_key_jwt` methods which involve sending a JWT * token * assertion signed with either a client secret or private key. * @@ -252,13 +252,13 @@ public static class Jwt { public Optional keyFile = Optional.empty(); /** - * If provided, indicates that JWT is signed using a private key from a key store + * If provided, indicates that JWT is signed using a private key from a keystore */ @ConfigItem public Optional keyStoreFile = Optional.empty(); /** - * A parameter to specify the password of the key store file. + * A parameter to specify the password of the keystore file. */ @ConfigItem public Optional keyStorePassword; @@ -289,7 +289,7 @@ public static class Jwt { public Optional tokenKeyId = Optional.empty(); /** - * Issuer of the signing key added as a JWT 'iss' claim (default: client id) + * Issuer of the signing key added as a JWT `iss` claim (default: client id) */ @ConfigItem public Optional issuer = Optional.empty(); @@ -441,41 +441,41 @@ public enum Verification { } /** - * Certificate validation and hostname verification, which can be one of the following values from enum - * {@link Verification}. Default is required. + * Certificate validation and hostname verification, which can be one of the following {@link Verification} values. + * Default is required. */ @ConfigItem public Optional verification = Optional.empty(); /** - * An optional key store which holds the certificate information instead of specifying separate files. + * An optional keystore which holds the certificate information instead of specifying separate files. */ @ConfigItem public Optional keyStoreFile = Optional.empty(); /** - * An optional parameter to specify type of the key store file. If not given, the type is automatically detected + * An optional parameter to specify type of the keystore file. If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional keyStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the key store file. If not given, the provider is automatically + * An optional parameter to specify a provider of the keystore file. If not given, the provider is automatically * detected - * based on the key store file type. + * based on the keystore file type. */ @ConfigItem public Optional keyStoreProvider; /** - * A parameter to specify the password of the key store file. If not given, the default ("password") is used. + * A parameter to specify the password of the keystore file. If not given, the default ("password") is used. */ @ConfigItem public Optional keyStorePassword; /** - * An optional parameter to select a specific key in the key store. When SNI is disabled, if the key store contains + * An optional parameter to select a specific key in the keystore. When SNI is disabled, if the keystore contains * multiple * keys and no alias is specified, the behavior is undefined. */ @@ -489,34 +489,34 @@ public enum Verification { public Optional keyStoreKeyPassword = Optional.empty(); /** - * An optional trust store which holds the certificate information of the certificates to trust + * An optional truststore which holds the certificate information of the certificates to trust */ @ConfigItem public Optional trustStoreFile = Optional.empty(); /** - * A parameter to specify the password of the trust store file. + * A parameter to specify the password of the truststore file. */ @ConfigItem public Optional trustStorePassword = Optional.empty(); /** - * A parameter to specify the alias of the trust store certificate. + * A parameter to specify the alias of the truststore certificate. */ @ConfigItem public Optional trustStoreCertAlias = Optional.empty(); /** - * An optional parameter to specify type of the trust store file. If not given, the type is automatically detected + * An optional parameter to specify type of the truststore file. If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional trustStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the trust store file. If not given, the provider is automatically + * An optional parameter to specify a provider of the truststore file. If not given, the provider is automatically * detected - * based on the trust store file type. + * based on the truststore file type. */ @ConfigItem public Optional trustStoreProvider; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java index b9e2edd863702d..2a25611394c301 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java @@ -22,8 +22,8 @@ public class OidcBuildTimeConfig { public DevUiConfig devui; /** * Enable the registration of the Default TokenIntrospection and UserInfo Cache implementation bean. - * Note it only allows to use the default implementation, one needs to configure it in order to activate it, - * please see {@link OidcConfig#tokenCache}. + * Note: This only enables the default implementation. It requires configuration to be activated. + * See {@link OidcConfig#tokenCache}. */ @ConfigItem(defaultValue = "true") public boolean defaultTokenCacheEnabled; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 624675b4fd05c9..05a69f6df10150 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -23,11 +23,11 @@ public class DevServicesConfig { public boolean enabled = true; /** - * The container image name to use, for container based DevServices providers. + * The container image name to use, for container-based DevServices providers. * * Image with a Quarkus based distribution is used by default. * Image with a WildFly based distribution can be selected instead, for example: - * 'quay.io/keycloak/keycloak:19.0.3-legacy'. + * `quay.io/keycloak/keycloak:19.0.3-legacy`. *

* Note Keycloak Quarkus and Keycloak WildFly images are initialized differently. * By default, Dev Services for Keycloak will assume it is a Keycloak Quarkus image if the image version does not end with a @@ -106,17 +106,17 @@ public class DevServicesConfig { /** * The Keycloak realm name. - * This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist, - * default value is 'quarkus' in this case. - * If the realm file pointed to by the 'realm-path' property exists then it is still recommended to set this property - * for Dev Services for Keycloak to avoid parsing the realm file in order to determine the realm name. + * This property will be used to create the realm if the realm file pointed to by the `realm-path` property does not exist, + * default value is `quarkus` in this case. + * If the realm file pointed to by the `realm-path` property exists then it is still recommended to set this property + * for Dev Services for Keycloak to avoid parsing the realm file to determine the realm name. * */ @ConfigItem public Optional realmName; /** - * Indicates if the Keycloak realm has to be created when the realm file pointed to by the 'realm-path' property does not + * Indicates if the Keycloak realm has to be created when the realm file pointed to by the `realm-path` property does not * exist. * * Disable it if you'd like to create a realm using Keycloak Administration Console @@ -128,7 +128,7 @@ public class DevServicesConfig { /** * The Keycloak users map containing the username and password pairs. * If this map is empty then two users, 'alice' and 'bob' with the passwords matching their names will be created. - * This property will be used to create the Keycloak users if the realm file pointed to by the 'realm-path' property does + * This property will be used to create the Keycloak users if the realm file pointed to by the `realm-path` property does * not exist. */ @ConfigItem @@ -138,7 +138,7 @@ public class DevServicesConfig { * The Keycloak user roles. * If this map is empty then a user named 'alice' will get 'admin' and 'user' roles and all other users will get a 'user' * role. - * This property will be used to create the Keycloak roles if the realm file pointed to by the 'realm-path' property does + * This property will be used to create the Keycloak roles if the realm file pointed to by the `realm-path` property does * not exist. */ @ConfigItem diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 48d25c15adfa43..db4eea48e8d00d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -40,7 +40,7 @@ public class OidcTenantConfig extends OidcCommonConfig { public boolean tenantEnabled = true; /** - * The application type, which can be one of the following values from enum {@link ApplicationType}. + * The application type, which can be one of the following {@link ApplicationType} values. */ @ConfigItem(defaultValueDocumentation = "service") public Optional applicationType = Optional.empty(); @@ -54,9 +54,9 @@ public class OidcTenantConfig extends OidcCommonConfig { public Optional authorizationPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC userinfo endpoint. + * Relative path or absolute URL of the OIDC UserInfo endpoint. * This property must only be set for the 'web-app' applications if OIDC discovery is disabled - * and 'authentication.user-info-required' property is enabled. + * and `authentication.user-info-required` property is enabled. * This property will be ignored if the discovery is enabled. */ @ConfigItem @@ -189,7 +189,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Allow caching the token introspection data. * Note enabling this property does not enable the cache itself but only permits to cache the token introspection - * for a given tenant. If the default token cache can be used then please see {@link OidcConfig.TokenCache} how to enable + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ @ConfigItem(defaultValue = "true") @@ -198,7 +198,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Allow caching the user info data. * Note enabling this property does not enable the cache itself but only permits to cache the user info data - * for a given tenant. If the default token cache can be used then please see {@link OidcConfig.TokenCache} how to enable + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ @ConfigItem(defaultValue = "true") @@ -689,7 +689,7 @@ public enum CookieSameSite { */ public enum ResponseMode { /** - * Authorization response parameters are encoded in the query string added to the redirect_uri + * Authorization response parameters are encoded in the query string added to the `redirect_uri` */ QUERY, @@ -707,19 +707,19 @@ public enum ResponseMode { public Optional responseMode = Optional.empty(); /** - * Relative path for calculating a "redirect_uri" query parameter. + * Relative path for calculating a `redirect_uri` query parameter. * It has to start from a forward slash and will be appended to the request URI's host and port. - * For example, if the current request URI is 'https://localhost:8080/service' then a 'redirect_uri' parameter + * For example, if the current request URI is 'https://localhost:8080/service' then a `redirect_uri` parameter * will be set to 'https://localhost:8080/' if this property is set to '/' and be the same as the request URI * if this property has not been configured. * Note the original request URI will be restored after the user has authenticated if 'restorePathAfterRedirect' is set - * to 'true'. + * to `true`. */ @ConfigItem public Optional redirectPath = Optional.empty(); /** - * If this property is set to 'true' then the original request URI which was used before + * If this property is set to `true` then the original request URI which was used before * the authentication will be restored after the user has been redirected back to the application. * * Note if `redirectPath` property is not set, the original request URI will be restored even if this property is @@ -737,8 +737,8 @@ public enum ResponseMode { /** * Relative path to the public endpoint which will process the error response from the OIDC authorization endpoint. - * If the user authentication has failed then the OIDC provider will return an 'error' and an optional - * 'error_description' + * If the user authentication has failed then the OIDC provider will return an `error` and an optional + * `error_description` * parameters, instead of the expected authorization 'code'. * * If this property is set then the user will be redirected to the endpoint which can return a user-friendly @@ -769,7 +769,7 @@ public enum ResponseMode { public boolean verifyAccessToken; /** - * Force 'https' as the 'redirect_uri' parameter scheme when running behind an SSL terminating reverse proxy. + * Force 'https' as the `redirect_uri` parameter scheme when running behind an SSL/TLS terminating reverse proxy. * This property, if enabled, will also affect the logout `post_logout_redirect_uri` and the local redirect requests. */ @ConfigItem(defaultValueDocumentation = "false") @@ -791,7 +791,7 @@ public enum ResponseMode { public boolean nonceRequired = false; /** - * Add the 'openid' scope automatically to the list of scopes. This is required for OpenId Connect providers + * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers * but will not work for OAuth2 providers such as Twitter OAuth2 which does not accept that scope and throws an error. */ @ConfigItem(defaultValueDocumentation = "true") @@ -811,8 +811,8 @@ public enum ResponseMode { public Optional> forwardParams = Optional.empty(); /** - * If enabled the state, session and post logout cookies will have their 'secure' parameter set to 'true' - * when HTTP is used. It may be necessary when running behind an SSL terminating reverse proxy. + * If enabled the state, session and post logout cookies will have their 'secure' parameter set to `true` + * when HTTP is used. It may be necessary when running behind an SSL/TLS terminating reverse proxy. * The cookies will always be secure if HTTPS is used even if this property is set to false. */ @ConfigItem(defaultValue = "false") @@ -820,8 +820,8 @@ public enum ResponseMode { /** * Cookie name suffix. - * For example, a session cookie name for the default OIDC tenant is 'q_session' but can be changed to 'q_session_test' - * if this property is set to 'test'. + * For example, a session cookie name for the default OIDC tenant is `q_session` but can be changed to `q_session_test` + * if this property is set to `test`. */ @ConfigItem public Optional cookieSuffix = Optional.empty(); @@ -861,8 +861,7 @@ public enum ResponseMode { * However, if multiple authentications are attempted from the same browser, for example, from the different * browser tabs, then the currently available state cookie may represent the authentication flow * initiated from another tab and not related to the current request. - * Disable this property if you would like to avoid supporting multiple authorization code flows running in the same - * browser. + * Disable this property to permit only a single authorization code flow in the same browser. * */ @ConfigItem(defaultValue = "true") @@ -886,14 +885,14 @@ public enum ResponseMode { * with {@link #redirectPath} may be needed to avoid such errors. *

* However, setting this property to `false` may help if the above options are not suitable. - * It will cause a new authentication redirect to OpenId Connect provider. Please be aware doing so may increase the + * It will cause a new authentication redirect to OpenId Connect provider. Doing so may increase the * risk of browser redirect loops. */ @ConfigItem(defaultValue = "false") public boolean failOnMissingStateParam = false; /** - * If this property is set to 'true' then an OIDC UserInfo endpoint will be called. + * If this property is set to `true` then an OIDC UserInfo endpoint will be called. * This property will be enabled if `quarkus.oidc.roles.source` is `userinfo` * or `quarkus.oidc.token.verify-access-token-with-user-info` is `true` * or `quarkus.oidc.authentication.id-token-required` is set to `false`, @@ -906,7 +905,7 @@ public enum ResponseMode { * Session age extension in minutes. * The user session age property is set to the value of the ID token life-span by default and * the user will be redirected to the OIDC provider to re-authenticate once the session has expired. - * If this property is set to a non-zero value then the expired ID token can be refreshed before + * If this property is set to a non-zero value, then the expired ID token can be refreshed before * the session has expired. * This property will be ignored if the `token.refresh-expired` property has not been enabled. */ @@ -914,7 +913,7 @@ public enum ResponseMode { public Duration sessionAgeExtension = Duration.ofMinutes(5); /** - * If this property is set to 'true' then a normal 302 redirect response will be returned + * If this property is set to `true` then a normal 302 redirect response will be returned * if the request was initiated via JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be * (re)authenticated which may not be desirable for Single-page applications (SPA) since * it automatically following the redirect may not work given that OIDC authorization endpoints typically do not support @@ -1276,7 +1275,7 @@ public static Token fromAudience(String... audience) { } /** - * Expected issuer 'iss' claim value. + * Expected issuer `iss` claim value. * Note this property overrides the `issuer` property which may be set in OpenId Connect provider's well-known * configuration. * If the `iss` claim value varies depending on the host/IP address or tenant id of the provider then you may skip the @@ -1356,7 +1355,7 @@ public static Token fromAudience(String... audience) { public Optional age = Optional.empty(); /** - * Name of the claim which contains a principal name. By default, the 'upn', 'preferred_username' and `sub` claims are + * Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub` claims are * checked. */ @ConfigItem @@ -1418,7 +1417,7 @@ public static Token fromAudience(String... audience) { * the providers may not control the private decryption keys. * In such cases set this property to point to the file containing the decryption private key in * PEM or JSON Web Key (JWK) format. - * Note that if a 'private_key_jwt' client authentication method is used then the private key + * Note that if a `private_key_jwt` client authentication method is used then the private key * which is used to sign client authentication JWT tokens will be used to try to decrypt an encrypted ID token * if this property is not set. */ @@ -1428,7 +1427,7 @@ public static Token fromAudience(String... audience) { /** * Allow the remote introspection of JWT tokens when no matching JWK key is available. * - * Note this property is set to 'true' by default for backward-compatibility reasons and will be set to `false` + * Note this property is set to `true` by default for backward-compatibility reasons and will be set to `false` * instead in one of the next releases. * * Also note this property will be ignored if JWK endpoint URI is not available and introspecting the tokens is @@ -1627,7 +1626,7 @@ public void setSubjectRequired(boolean subjectRequired) { public static enum ApplicationType { /** - * A {@code WEB_APP} is a client that serves pages, usually a frontend application. For this type of client the + * A {@code WEB_APP} is a client that serves pages, usually a front-end application. For this type of client the * Authorization Code Flow is defined as the preferred method for authenticating users. */ WEB_APP, diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java index c1e2c9bfef298e..92c919c44fe800 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java @@ -4,6 +4,7 @@ import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; @@ -54,6 +55,7 @@ void resource() { assertEquals("authservice", server.getResource().getAttribute(AttributeKey.stringKey("service.name"))); assertEquals(config.getRawValue("quarkus.uuid"), server.getResource().getAttribute(AttributeKey.stringKey("service.instance.id"))); + assertNotNull(server.getResource().getAttribute(AttributeKey.stringKey("host.name"))); } @Path("/hello") diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java index cc9666624cd97f..de7e1ee998d961 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java @@ -3,6 +3,8 @@ import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; @@ -71,12 +73,21 @@ public Resource apply(Resource existingResource, ConfigProperties configProperti .filter(sn -> !sn.equals(appConfig.name.orElse("unset"))) .orElse(null); + // must be resolved at startup, once. + String hostname = null; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "unknown"; + } + // Merge resource instances with env attributes Resource resource = resources.stream() .reduce(Resource.empty(), Resource::merge) .merge(TracerUtil.mapResourceAttributes( oTelRuntimeConfig.resourceAttributes().orElse(emptyList()), - serviceName)); // from properties + serviceName, // from properties + hostname)); return consolidatedResource.merge(resource); } else { return Resource.builder().build(); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java index 84ebd239996fa2..9c8e915904a04d 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.runtime.tracing; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.HOST_NAME; import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; import java.util.List; @@ -14,7 +15,7 @@ public class TracerUtil { private TracerUtil() { } - public static Resource mapResourceAttributes(List resourceAttributes, String serviceName) { + public static Resource mapResourceAttributes(List resourceAttributes, String serviceName, String hostname) { final AttributesBuilder attributesBuilder = Attributes.builder(); if (!resourceAttributes.isEmpty()) { @@ -27,6 +28,10 @@ public static Resource mapResourceAttributes(List resourceAttributes, St attributesBuilder.put(SERVICE_NAME.getKey(), serviceName); } + if (hostname != null) { + attributesBuilder.put(HOST_NAME, hostname); + } + return Resource.create(attributesBuilder.build()); } } diff --git a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java index c3b27767970ef7..99c005ce5ed91a 100644 --- a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java +++ b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java @@ -19,7 +19,7 @@ public void testMapResourceAttributes() { "service.namespace=mynamespace", "service.version=1.0", "deployment.environment=production"); - Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes, null); + Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes, null, null); Attributes attributes = resource.getAttributes(); Assertions.assertThat(attributes.size()).isEqualTo(4); Assertions.assertThat(attributes.get(ResourceAttributes.SERVICE_NAME)).isEqualTo("myservice"); 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 69d292bd816340..3899c03b691693 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 @@ -5,6 +5,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -216,6 +217,7 @@ public void defineHealthRoutes(BuildProducer routes, .routeConfigKey("quarkus.smallrye-health.root-path") .handler(new SmallRyeHealthHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the liveness handler @@ -224,6 +226,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the readiness handler @@ -232,14 +235,29 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); + // Find all health groups + Set healthGroups = new HashSet<>(); + // with simple @HealthGroup annotations + for (AnnotationInstance healthGroupAnnotation : index.getAnnotations(HEALTH_GROUP)) { + healthGroups.add(healthGroupAnnotation.value().asString()); + } + // with @HealthGroups repeatable annotations + for (AnnotationInstance healthGroupsAnnotation : index.getAnnotations(HEALTH_GROUPS)) { + for (AnnotationInstance healthGroupAnnotation : healthGroupsAnnotation.value().asNestedArray()) { + healthGroups.add(healthGroupAnnotation.value().asString()); + } + } + // Register the health group handlers routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .management("quarkus.smallrye-health.management.enabled") .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); @@ -248,6 +266,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/*") .handler(handler) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the wellness handler @@ -256,6 +275,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the startup handler @@ -264,6 +284,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.startupPath) .handler(new SmallRyeStartupHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java index 95b87746c1b088..84c5c6fa62d0cb 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthGroupsAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealthGroups(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java index 6d9d33066e8fbd..6960bb284bce9b 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealth(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java index e9993754187690..fff1485398fbc1 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java @@ -10,11 +10,7 @@ import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; -import io.smallrye.mutiny.vertx.MutinyHelper; -import io.vertx.core.Context; import io.vertx.core.Handler; -import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; @@ -22,7 +18,7 @@ abstract class SmallRyeHealthHandlerBase implements Handler { - protected abstract Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); + protected abstract SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); @Override public void handle(RoutingContext ctx) { @@ -45,21 +41,19 @@ private void doHandle(RoutingContext ctx) { Arc.container().instance(CurrentIdentityAssociation.class).get().setIdentity(user.getSecurityIdentity()); } SmallRyeHealthReporter reporter = Arc.container().instance(SmallRyeHealthReporter.class).get(); - Context context = Vertx.currentContext(); - getHealth(reporter, ctx).emitOn(MutinyHelper.executor(context)) - .subscribe().with(health -> { - HttpServerResponse resp = ctx.response(); - if (health.isDown()) { - resp.setStatusCode(503); - } - resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); - Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks - try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { - reporter.reportHealth(outputStream, health); - resp.end(buffer); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + SmallRyeHealth health = getHealth(reporter, ctx); + HttpServerResponse resp = ctx.response(); + if (health.isDown()) { + resp.setStatusCode(503); + } + resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); + Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks + try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { + reporter.reportHealth(outputStream, health); + resp.end(buffer); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } + } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java index e0c7ba38744399..66f960791ad8d8 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java @@ -2,14 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeIndividualHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { String group = ctx.normalizedPath().substring(ctx.normalizedPath().lastIndexOf("/") + 1); - return reporter.getHealthGroupAsync(group); + return reporter.getHealthGroup(group); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java index ad33e824ff3d71..a5cf3dd904cbe9 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeLivenessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getLivenessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getLiveness(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java index 18c652bd673bd7..a23a3e1f9d5383 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeReadinessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getReadinessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getReadiness(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java index cd1ae14846cc97..c450430735ecb8 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeStartupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getStartupAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getStartup(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java index e2131f51de416e..84ca3860c1caed 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeWellnessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getWellnessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getWellness(); } } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js index f382cbdf19ce2a..2f7abd6e6fcd58 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js @@ -61,7 +61,7 @@ export class QwcSmallryeReactiveMessagingChannels extends LitElement { > @@ -95,9 +95,19 @@ export class QwcSmallryeReactiveMessagingChannels extends LitElement { } _channelPublisherRenderer(channel) { - const publisher = channel.publisher; - if (publisher) { - return this._renderComponent(publisher); + const publishers = channel.publishers; + if (publishers) { + if (publishers.length === 1) { + return this._renderComponent(publishers[0]); + } else if (publishers.length > 1) { + return html` +

    + ${publishers.map(item => html`
  • ${this._renderComponent(item)}
  • `)} +
+ `; + } else { + return html`No publishers` + } } } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java index 6f79ad41b86e17..3fc7884e69be0a 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java @@ -34,21 +34,24 @@ public List get() { .get(); // collect all channels - Map publishers = new HashMap<>(); + Map> publishers = new HashMap<>(); Map> consumers = new HashMap<>(); Function> fun = e -> new ArrayList<>(); // Unfortunately, there is no easy way to obtain the connectors metadata Connectors connectors = container.instance(Connectors.class).get(); - publishers.putAll(connectors.outgoingConnectors); + for (Entry entry : connectors.outgoingConnectors.entrySet()) { + publishers.computeIfAbsent(entry.getKey(), fun) + .add(entry.getValue()); + } for (Entry entry : connectors.incomingConnectors.entrySet()) { consumers.computeIfAbsent(entry.getKey(), fun) .add(entry.getValue()); } for (EmitterConfiguration emitter : context.getEmitterConfigurations()) { - publishers.put(emitter.name(), - new Component(ComponentType.EMITTER, + publishers.computeIfAbsent(emitter.name(), fun) + .add(new Component(ComponentType.EMITTER, emitter.broadcast() ? "@Broadcast " : "" + asCode(DevConsoleRecorder.EMITTERS.get(emitter.name())))); } @@ -58,23 +61,27 @@ public List get() { asCode(DevConsoleRecorder.CHANNELS.get(channel.channelName)))); } for (MediatorConfiguration mediator : context.getMediatorConfigurations()) { - boolean isProcessor = !mediator.getIncoming().isEmpty() && mediator.getOutgoing() != null; + boolean isProcessor = !mediator.getIncoming().isEmpty() && !mediator.getOutgoings().isEmpty(); if (isProcessor) { - publishers.put(mediator.getOutgoing(), - new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); + for (String outgoing : mediator.getOutgoings()) { + publishers.computeIfAbsent(outgoing, fun) + .add(new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); + } for (String incoming : mediator.getIncoming()) { consumers.computeIfAbsent(incoming, fun) .add(new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); } - } else if (mediator.getOutgoing() != null) { - StringBuilder builder = new StringBuilder(); - builder.append(asMethod(mediator.methodAsString())); - if (mediator.getBroadcast()) { - builder.append("[broadcast: true]"); + } else if (!mediator.getOutgoings().isEmpty()) { + for (String outgoing : mediator.getOutgoings()) { + StringBuilder builder = new StringBuilder(); + builder.append(asMethod(mediator.methodAsString())); + if (mediator.getBroadcast()) { + builder.append("[broadcast: true]"); + } + publishers.computeIfAbsent(outgoing, fun) + .add(new Component(ComponentType.PUBLISHER, builder.toString())); } - publishers.put(mediator.getOutgoing(), - new Component(ComponentType.PUBLISHER, builder.toString())); } else if (!mediator.getIncoming().isEmpty()) { for (String incoming : mediator.getIncoming()) { consumers.computeIfAbsent(incoming, fun) @@ -113,12 +120,12 @@ public List getChannels() { public static class DevChannelInfo implements Comparable { private final String name; - private final Component publisher; + private final List publishers; private final List consumers; - public DevChannelInfo(String name, Component publisher, List consumers) { + public DevChannelInfo(String name, List publishers, List consumers) { this.name = name; - this.publisher = publisher; + this.publishers = publishers != null ? publishers : Collections.emptyList(); this.consumers = consumers != null ? consumers : Collections.emptyList(); } @@ -126,8 +133,8 @@ public String getName() { return name; } - public Component getPublisher() { - return publisher; + public List getPublishers() { + return publishers; } public List getConsumers() { @@ -136,17 +143,11 @@ public List getConsumers() { @Override public int compareTo(DevChannelInfo other) { - if (publisher != other.publisher) { - if (other.publisher == null) { - return -1; - } - if (publisher == null) { - return 1; - } - // publisher connectors first - if (publisher.type != other.publisher.type) { - return publisher.type == ComponentType.CONNECTOR ? -1 : 1; - } + // publisher connectors last + long publisherConnectors = publishers.stream().filter(Component::isConnector).count(); + long otherPublisherConnectors = other.publishers.stream().filter(Component::isConnector).count(); + if (publisherConnectors != otherPublisherConnectors) { + return Long.compare(otherPublisherConnectors, publisherConnectors); } // consumer connectors last long consumerConnectors = consumers.stream().filter(Component::isConnector).count(); @@ -154,10 +155,6 @@ public int compareTo(DevChannelInfo other) { if (consumerConnectors != otherConsumersConnectors) { return Long.compare(otherConsumersConnectors, consumerConnectors); } - if (publisher != other.publisher && publisher.type == ComponentType.CONNECTOR - && other.publisher.type != ComponentType.CONNECTOR) { - return 1; - } // alphabetically return name.compareTo(other.name); } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java index 3e83d74354348d..57554fb7e17853 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java @@ -24,7 +24,7 @@ public JsonArray getInfo() { private JsonObject toJson(DevReactiveMessagingInfos.DevChannelInfo channel) { JsonObject json = new JsonObject(); json.put("name", channel.getName()); - json.put("publisher", toJson(channel.getPublisher())); + json.put("publishers", toJson(channel.getPublishers())); json.put("consumers", toJson(channel.getConsumers())); return json; } 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 84b2aaca206dcd..fb34fd418ef281 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 @@ -1,28 +1,21 @@ package io.quarkus.vertx.http.deployment; import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; +import static io.quarkus.arc.processor.DotNames.SINGLETON; -import java.security.Permission; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Singleton; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -30,185 +23,70 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.configuration.ConfigurationException; -import io.quarkus.security.StringPermission; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; import io.quarkus.vertx.http.runtime.security.VertxBlockingSecurityExecutor; import io.vertx.core.http.ClientAuth; import io.vertx.ext.web.RoutingContext; public class HttpSecurityProcessor { - @BuildStep @Record(ExecutionTime.STATIC_INIT) - public void builtins(BuildProducer producer, - BuildProducer reflectiveClassProducer, - CombinedIndexBuildItem combinedIndexBuildItem, - HttpBuildTimeConfig buildTimeConfig, HttpSecurityRecorder recorder, - BuildProducer beanProducer) { - producer.produce(new HttpSecurityPolicyBuildItem("deny", new SupplierImpl<>(new DenySecurityPolicy()))); - producer.produce(new HttpSecurityPolicyBuildItem("permit", new SupplierImpl<>(new PermitSecurityPolicy()))); - producer.produce( - new HttpSecurityPolicyBuildItem("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy()))); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); - } - Map> permClassToCreator = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - PolicyConfig policyConfig = e.getValue(); - if (policyConfig.permissions.isEmpty()) { - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed)))); - } else { - // create HTTP Security policy that checks allowed roles and grants SecurityIdentity permissions to - // requests that this policy allows to proceed - var permissionCreator = permClassToCreator.computeIfAbsent(policyConfig.permissionClass, - new Function>() { - @Override - public BiFunction apply(String s) { - if (StringPermission.class.getName().equals(s)) { - return recorder.stringPermissionCreator(); - } - boolean constructorAcceptsActions = validateConstructor(combinedIndexBuildItem.getIndex(), - policyConfig.permissionClass); - return recorder.customPermissionCreator(s, constructorAcceptsActions); - } - }); - var policy = recorder.createRolesAllowedPolicy(policyConfig.rolesAllowed, policyConfig.permissions, - permissionCreator); - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), policy)); - } - } - - if (!permClassToCreator.isEmpty()) { - // we need to register Permission classes for reflection as strictly speaking - // they might not exactly match classes defined via `PermissionsAllowed#permission` - var permissionClassesArr = permClassToCreator.keySet().toArray(new String[0]); - reflectiveClassProducer - .produce(ReflectiveClassBuildItem.builder(permissionClassesArr).constructors().fields().methods().build()); - } - } - - private static boolean validateConstructor(IndexView index, String permissionClass) { - ClassInfo classInfo = index.getClassByName(permissionClass); - - if (classInfo == null) { - throw new ConfigurationException(String.format("Permission class '%s' is missing", permissionClass)); - } - - // must have exactly one constructor - if (classInfo.constructors().size() != 1) { - throw new ConfigurationException( - String.format("Permission class '%s' must have exactly one constructor", permissionClass)); - } - MethodInfo constructor = classInfo.constructors().get(0); - - // first parameter must be permission name (String) - if (constructor.parametersCount() == 0 || !isString(constructor.parameterType(0))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", - permissionClass, String.class.getName())); - } - - // second parameter (actions) is optional - if (constructor.parametersCount() == 1) { - // permission constructor accepts just name, no actions - return false; - } - - if (constructor.parametersCount() == 2) { - if (!isStringArray(constructor.parameterType(1))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor second parameter must be '%s' array", permissionClass, - String.class.getName())); - } - return true; + @BuildStep + void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, + HttpSecurityRecorder recorder) { + if (!httpSecurityPolicyBuildItems.isEmpty()) { + recorder.setBuildTimeNamedPolicies(httpSecurityPolicyBuildItems.stream().collect( + Collectors.toMap(HttpSecurityPolicyBuildItem::getName, HttpSecurityPolicyBuildItem::getPolicySupplier))); } - - throw new ConfigurationException(String.format( - "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", - permissionClass)); - } - - private static boolean isStringArray(Type type) { - return type.kind() == Type.Kind.ARRAY && isString(type.asArrayType().constituent()); - } - - private static boolean isString(Type type) { - return type.kind() == Type.Kind.CLASS && type.name().toString().equals(String.class.getName()); } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initFormAuth( + @Record(ExecutionTime.STATIC_INIT) + AdditionalBeanBuildItem initFormAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, BuildProducer filterBuildItemBuildProducer) { - if (!buildTimeConfig.auth.proactive) { - filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) - .handler(recorder.formAuthPostHandler()).build()); - } if (buildTimeConfig.auth.form.enabled) { - return SyntheticBeanBuildItem.configure(FormAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupFormAuth()).done(); + if (!buildTimeConfig.auth.proactive) { + filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) + .handler(recorder.formAuthPostHandler()).build()); + } + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(FormAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initMtlsClientAuth( - HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig) { + AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) { if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) { - return SyntheticBeanBuildItem.configure(MtlsAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupMtlsClientAuth()).done(); + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(MtlsAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, - ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, BuildProducer securityInformationProducer) { - if (!applicationBasicAuthRequired(buildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth(buildTimeConfig)); if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) @@ -247,18 +125,8 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, HttpBuildTimeConfig buildTimeConfig, - List httpSecurityPolicyBuildItemList, BuildProducer securityInformationProducer) { - Map> policyMap = new HashMap<>(); - for (HttpSecurityPolicyBuildItem e : httpSecurityPolicyBuildItemList) { - if (policyMap.containsKey(e.getName())) { - throw new RuntimeException("Multiple HTTP security policies defined with name " + e.getName()); - } - policyMap.put(e.getName(), e.policySupplier); - } - if (!buildTimeConfig.auth.form.enabled && buildTimeConfig.auth.basic.orElse(false)) { securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } @@ -270,21 +138,13 @@ void setupAuthenticationMechanisms( beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(HttpAuthenticator.class) .addBeanClass(HttpAuthorizer.class).build()); + beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); filterBuildItemBuildProducer .produce(new FilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), FilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer .produce(new FilterBuildItem(recorder.permissionCheckHandler(), FilterBuildItem.AUTHORIZATION)); - - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } @@ -325,4 +185,18 @@ void produceEagerSecurityInterceptorStorage(HttpSecurityRecorder recorder, private static boolean isMtlsClientAuthenticationEnabled(HttpBuildTimeConfig buildTimeConfig) { return !ClientAuth.NONE.equals(buildTimeConfig.tlsClientAuth); } + + static class IsApplicationBasicAuthRequired implements BooleanSupplier { + private final boolean required; + + public IsApplicationBasicAuthRequired(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + required = applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig); + } + + @Override + public boolean getAsBoolean() { + return required; + } + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java index 07d36632f97ae4..fc7d1e31a0b6b2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java @@ -1,13 +1,8 @@ package io.quarkus.vertx.http.deployment; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - import jakarta.inject.Singleton; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -15,47 +10,26 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.deployment.HttpSecurityProcessor.IsApplicationBasicAuthRequired; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceSecurityRecorder; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; public class ManagementInterfaceSecurityProcessor { - @BuildStep - public void builtins(ManagementInterfaceBuildTimeConfig buildTimeConfig, - BuildProducer beanProducer) { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(ManagementPathMatchingHttpSecurityPolicy.class)); - } - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIfNot = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( - HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceSecurityRecorder recorder, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { - if (HttpSecurityProcessor.applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - if (managementInterfaceBuildTimeConfig.auth.basic.orElse(false)) { SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth()); return configurator.done(); @@ -71,39 +45,21 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, ManagementInterfaceBuildTimeConfig buildTimeConfig) { - - Map> policyMap = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - policyMap.put(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed))); - } - policyMap.put("deny", new SupplierImpl<>(new DenySecurityPolicy())); - policyMap.put("permit", new SupplierImpl<>(new PermitSecurityPolicy())); - policyMap.put("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy())); - if (buildTimeConfig.auth.basic.orElse(false) && capabilities.isPresent(Capability.SECURITY)) { beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClass(HttpAuthenticator.class) + .addBeanClass(ManagementPathMatchingHttpSecurityPolicy.class) .addBeanClass(ManagementInterfaceHttpAuthorizer.class).build()); filterBuildItemBuildProducer .produce(new ManagementInterfaceFilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), ManagementInterfaceFilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer - .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(buildTimeConfig, policyMap), + .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(), ManagementInterfaceFilterBuildItem.AUTHORIZATION)); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index 3e609d66f98257..52b9017a38daaf 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -1,6 +1,5 @@ package io.quarkus.vertx.http.runtime; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -32,18 +31,6 @@ public class AuthConfig { @ConfigItem public Optional realm; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java new file mode 100644 index 00000000000000..eee0b3f84d8970 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -0,0 +1,25 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Authentication mechanism information used for configuring HTTP auth instance for the deployment. + */ +@ConfigGroup +public class AuthRuntimeConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 89ffdf53d0c198..e726692e1952b5 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -14,6 +14,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class HttpConfiguration { + /** + * Authentication configuration + */ + public AuthRuntimeConfig auth; + /** * Enable the CORS filter. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java index 7d16bc2f4e392d..6977b99f08770b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java @@ -36,6 +36,7 @@ public class PolicyConfig { * Permissions granted by this policy will be created with a `java.security.Permission` implementation * specified by this configuration property. The permission class must declare exactly one constructor * that accepts permission name (`String`) or permission name and actions (`String`, `String[]`). + * Permission class must be registered for reflection if you run your application in a native mode. */ @ConfigItem(defaultValue = "io.quarkus.security.StringPermission") public String permissionClass = StringPermission.class.getName(); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java index 017fcfe953a666..a22db7e1393599 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java @@ -1,12 +1,9 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.vertx.http.runtime.PolicyConfig; -import io.quarkus.vertx.http.runtime.PolicyMappingConfig; /** * Authentication for the management interface. @@ -20,18 +17,6 @@ public class ManagementAuthConfig { @ConfigItem public Optional basic; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java index 9d77f458d1c901..7a2236a17f240a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java @@ -23,6 +23,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "management") public class ManagementInterfaceConfiguration { + /** + * Authentication configuration + */ + public ManagementRuntimeAuthConfig auth; + /** * The HTTP port */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java index 03e6be2d83742e..f3b1b9f5a3c331 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java @@ -1,18 +1,13 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.vertx.http.runtime.security.AbstractPathMatchingHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.AbstractAuthenticationHandler; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; @@ -22,21 +17,11 @@ @Recorder public class ManagementInterfaceSecurityRecorder { - final RuntimeValue httpConfiguration; - final ManagementInterfaceBuildTimeConfig buildTimeConfig; - - public ManagementInterfaceSecurityRecorder(RuntimeValue httpConfiguration, - ManagementInterfaceBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } - public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new ManagementAuthenticationHandler(proactiveAuthentication); } - public Handler permissionCheckHandler(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { + public Handler permissionCheckHandler() { return new Handler() { volatile ManagementInterfaceHttpAuthorizer authorizer; @@ -52,17 +37,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(ManagementPathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - public Supplier setupBasicAuth() { return new Supplier() { @Override @@ -91,5 +65,10 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(ManagementInterfaceConfiguration.class).get().auth.permissions.isEmpty(); + } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java new file mode 100644 index 00000000000000..f9002d619a0815 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.runtime.PolicyMappingConfig; + +/** + * Authentication for the management interface. + */ +@ConfigGroup +public class ManagementRuntimeAuthConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index 14aa4d607cd29e..fa82007a034a3a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -1,6 +1,11 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; + +import java.lang.reflect.InvocationTargetException; +import java.security.Permission; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -8,9 +13,11 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import java.util.function.Supplier; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.PolicyMappingConfig; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult; @@ -26,6 +33,11 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); + AbstractPathMatchingHttpSecurityPolicy(Map permissions, + Map rolePolicy, String rootPath, Map namedBuildTimePolicies) { + init(permissions, toNamedHttpSecPolicies(rolePolicy, namedBuildTimePolicies), rootPath); + } + public String getAuthMechanismName(RoutingContext routingContext) { PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { @@ -79,13 +91,8 @@ public Uni apply(CheckResult checkResult) { }); } - public void init(Map permissions, - Map> supplierMap, String rootPath) { - Map permissionCheckers = new HashMap<>(); - for (Map.Entry> i : supplierMap.entrySet()) { - permissionCheckers.put(i.getKey(), i.getValue().get()); - } - + private void init(Map permissions, + Map permissionCheckers, String rootPath) { Map> tempMap = new HashMap<>(); for (Map.Entry entry : permissions.entrySet()) { HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy); @@ -150,6 +157,157 @@ public List findPermissionCheckers(RoutingContext context) { } + private static Map toNamedHttpSecPolicies(Map rolePolicies, + Map namedBuildTimePolicies) { + Map namedPolicies = new HashMap<>(); + if (!namedBuildTimePolicies.isEmpty()) { + namedPolicies.putAll(namedBuildTimePolicies); + } + for (Map.Entry e : rolePolicies.entrySet()) { + PolicyConfig policyConfig = e.getValue(); + if (policyConfig.permissions.isEmpty()) { + namedPolicies.put(e.getKey(), new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed)); + } else { + final Map> roleToPermissions = new HashMap<>(); + for (Map.Entry> roleToPermissionStr : policyConfig.permissions.entrySet()) { + + // collect permission actions + // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 + Map cache = new HashMap<>(); + final String role = roleToPermissionStr.getKey(); + for (String permissionToAction : roleToPermissionStr.getValue()) { + // parse permission to actions and add it to cache + addPermissionToAction(cache, role, permissionToAction); + } + + // create permissions + var permissions = new HashSet(); + for (PermissionToActions helper : cache.values()) { + if (StringPermission.class.getName().equals(policyConfig.permissionClass)) { + permissions.add(new StringPermission(helper.permissionName, helper.actions.toArray(new String[0]))); + } else { + permissions.add(customPermissionCreator(policyConfig, helper)); + } + } + + roleToPermissions.put(role, Set.copyOf(permissions)); + } + namedPolicies.put(e.getKey(), + new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed, Map.copyOf(roleToPermissions))); + } + } + namedPolicies.put("deny", new DenySecurityPolicy()); + namedPolicies.put("permit", new PermitSecurityPolicy()); + namedPolicies.put("authenticated", new AuthenticatedHttpSecurityPolicy()); + return namedPolicies; + } + + private static boolean acceptsActions(String permissionClassStr) { + var permissionClass = loadClass(permissionClassStr); + if (permissionClass.getConstructors().length != 1) { + throw new ConfigurationException( + String.format("Permission class '%s' must have exactly one constructor", permissionClass)); + } + var constructor = permissionClass.getConstructors()[0]; + // first parameter must be permission name (String) + if (constructor.getParameterCount() == 0 || !(constructor.getParameterTypes()[0] == String.class)) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", + permissionClass, String.class.getName())); + } + final boolean acceptsActions; + if (constructor.getParameterCount() == 1) { + acceptsActions = false; + } else { + if (constructor.getParameterCount() == 2) { + if (constructor.getParameterTypes()[1] != String[].class) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor second parameter must be '%s' array", + permissionClass, + String.class.getName())); + } + } else { + throw new ConfigurationException(String.format( + "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", + permissionClass)); + } + acceptsActions = true; + } + return acceptsActions; + } + + private static void addPermissionToAction(Map cache, String role, String permissionToAction) { + final String permissionName; + final String action; + // incoming value is either in format perm1:action1 or perm1 (with or withot action) + if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { + // perm1:action1 + var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); + if (permToActions.length != 2) { + throw new ConfigurationException( + String.format("Invalid permission format '%s', please use exactly one permission to action separator", + permissionToAction)); + } + permissionName = permToActions[0].trim(); + action = permToActions[1].trim(); + } else { + // perm1 + permissionName = permissionToAction.trim(); + action = null; + } + + if (permissionName.isEmpty()) { + throw new ConfigurationException( + String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); + } + + cache.computeIfAbsent(permissionName, new Function() { + @Override + public PermissionToActions apply(String s) { + return new PermissionToActions(s); + } + }).addAction(action); + } + + private static Class loadClass(String className) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); + } + } + + private static Permission customPermissionCreator(PolicyConfig policyConfig, PermissionToActions helper) { + try { + var constructor = loadClass(policyConfig.permissionClass).getConstructors()[0]; + if (acceptsActions(policyConfig.permissionClass)) { + return (Permission) constructor.newInstance(helper.permissionName, helper.actions.toArray(new String[0])); + } else { + return (Permission) constructor.newInstance(helper.permissionName); + } + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", + policyConfig.permissionClass, helper.permissionName, + Arrays.toString(helper.actions.toArray(new String[0]))), e); + } + } + + private static final class PermissionToActions { + private final String permissionName; + private final Set actions; + + private PermissionToActions(String permissionName) { + this.permissionName = permissionName; + this.actions = new HashSet<>(); + } + + private void addAction(String action) { + if (action != null) { + this.actions.add(action); + } + } + } + static class HttpMatcher { final String authMechanism; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 85927ed2f89229..48bfff2708aadb 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -1,12 +1,16 @@ package io.quarkus.vertx.http.runtime.security; import java.net.URI; +import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import jakarta.inject.Inject; + import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpHeaderNames; @@ -18,6 +22,9 @@ import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.vertx.http.runtime.FormAuthConfig; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.Handler; @@ -47,6 +54,44 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism private final PersistentLoginManager loginManager; + //the temp encryption key, persistent across dev mode restarts + static volatile String encryptionKey; + + @Inject + FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { + String key; + if (!httpConfiguration.encryptionKey.isPresent()) { + if (encryptionKey != null) { + //persist across dev mode restarts + key = encryptionKey; + } else { + byte[] data = new byte[32]; + new SecureRandom().nextBytes(data); + key = encryptionKey = Base64.getEncoder().encodeToString(data); + log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); + } + } else { + key = httpConfiguration.encryptionKey.get(); + } + FormAuthConfig form = buildTimeConfig.auth.form; + this.loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), + form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), + form.cookiePath.orElse(null)); + this.loginPage = startWithSlash(form.loginPage.orElse(null)); + this.errorPage = startWithSlash(form.errorPage.orElse(null)); + this.landingPage = startWithSlash(form.landingPage.orElse(null)); + this.postLocation = startWithSlash(form.postLocation); + this.usernameParameter = form.usernameParameter; + this.passwordParameter = form.passwordParameter; + this.locationCookie = form.locationCookie; + this.cookiePath = form.cookiePath.orElse(null); + boolean redirectAfterLogin = form.redirectAfterLogin; + this.redirectToLandingPage = landingPage != null && redirectAfterLogin; + this.redirectToLoginPage = loginPage != null; + this.redirectToErrorPage = errorPage != null; + this.cookieSameSite = CookieSameSite.valueOf(form.cookieSameSite.name()); + } + public FormAuthenticationMechanism(String loginPage, String postLocation, String usernameParameter, String passwordParameter, String errorPage, String landingPage, boolean redirectAfterLogin, String locationCookie, String cookieSameSite, String cookiePath, @@ -240,4 +285,11 @@ public Set> getCredentialTypes() { public Uni getCredentialTransport(RoutingContext context) { return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM)); } + + private static String startWithSlash(String page) { + if (page == null) { + return null; + } + return page.startsWith("/") ? page : "/" + page; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 84b3ef16064ade..40cc75234ddce4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -1,20 +1,9 @@ package io.quarkus.vertx.http.runtime.security; -import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; - -import java.lang.reflect.InvocationTargetException; -import java.security.Permission; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Base64; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletionException; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -24,19 +13,14 @@ import org.jboss.logging.Logger; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; -import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; -import io.quarkus.vertx.http.runtime.FormAuthConfig; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; @@ -52,23 +36,6 @@ public class HttpSecurityRecorder { private static final Logger log = Logger.getLogger(HttpSecurityRecorder.class); - protected static final Consumer NOOP_CALLBACK = new Consumer() { - @Override - public void accept(Throwable throwable) { - - } - }; - - final RuntimeValue httpConfiguration; - final HttpBuildTimeConfig buildTimeConfig; - - //the temp encryption key, persistent across dev mode restarts - static volatile String encryptionKey; - - public HttpSecurityRecorder(RuntimeValue httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new HttpAuthenticationHandler(proactiveAuthentication); @@ -88,64 +55,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(HttpBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(PathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - - public Supplier setupFormAuth() { - - return new Supplier() { - - @Override - public FormAuthenticationMechanism get() { - String key; - if (!httpConfiguration.getValue().encryptionKey.isPresent()) { - if (encryptionKey != null) { - //persist across dev mode restarts - key = encryptionKey; - } else { - byte[] data = new byte[32]; - new SecureRandom().nextBytes(data); - key = encryptionKey = Base64.getEncoder().encodeToString(data); - log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); - } - } else { - key = httpConfiguration.getValue().encryptionKey.get(); - } - FormAuthConfig form = buildTimeConfig.auth.form; - PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), - form.cookiePath.orElse(null)); - String loginPage = startWithSlash(form.loginPage.orElse(null)); - String errorPage = startWithSlash(form.errorPage.orElse(null)); - String landingPage = startWithSlash(form.landingPage.orElse(null)); - String postLocation = startWithSlash(form.postLocation); - String usernameParameter = form.usernameParameter; - String passwordParameter = form.passwordParameter; - String locationCookie = form.locationCookie; - String cookiePath = form.cookiePath.orElse(null); - boolean redirectAfterLogin = form.redirectAfterLogin; - return new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, - errorPage, landingPage, redirectAfterLogin, locationCookie, form.cookieSameSite.name(), cookiePath, - loginManager); - } - }; - } - - private static String startWithSlash(String page) { - if (page == null) { - return null; - } - return page.startsWith("/") ? page : "/" + page; - } - public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { return new Supplier() { @Override @@ -156,15 +65,6 @@ public BasicAuthenticationMechanism get() { }; } - public Supplier setupMtlsClientAuth() { - return new Supplier() { - @Override - public MtlsAuthenticationMechanism get() { - return new MtlsAuthenticationMechanism(); - } - }; - } - /** * This handler resolves the identity, and will be mapped to the post location. Otherwise, * for lazy auth the post will not be evaluated if there is no security rule for the post location. @@ -194,36 +94,6 @@ public void onFailure(Throwable throwable) { }; } - public BiFunction stringPermissionCreator() { - return StringPermission::new; - } - - public BiFunction customPermissionCreator(String clazz, boolean acceptsActions) { - return new BiFunction() { - @Override - public Permission apply(String name, String[] actions) { - try { - if (acceptsActions) { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name, actions); - } else { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name); - } - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException( - String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", clazz, - name, Arrays.toString(actions)), - e); - } - } - }; - } - - public Supplier createRolesAllowedPolicy(List rolesAllowed, - Map> roleToPermissionsStr, BiFunction permissionCreator) { - final Map> roleToPermissions = createPermissions(roleToPermissionsStr, permissionCreator); - return new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(rolesAllowed, roleToPermissions)); - } - public Supplier createSecurityInterceptorStorage( Map, Consumer> endpointRuntimeValToInterceptor) { @@ -240,63 +110,12 @@ public EagerSecurityInterceptorStorage get() { }; } - private static Map> createPermissions(Map> roleToPermissions, - BiFunction permissionCreator) { - // role -> created permissions - Map> result = new HashMap<>(); - for (Map.Entry> e : roleToPermissions.entrySet()) { - - // collect permission actions - // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 - Map cache = new HashMap<>(); - final String role = e.getKey(); - for (String permissionToAction : e.getValue()) { - // parse permission to actions and add it to cache - addPermissionToAction(cache, role, permissionToAction); - } - - // create permissions - var permissions = new HashSet(); - for (PermissionToActions permission : cache.values()) { - permissions.add(permission.create(permissionCreator)); - } - - result.put(role, Set.copyOf(permissions)); + public void setBuildTimeNamedPolicies(Map> buildTimeNamedPolicies) { + Map nameToPolicy = new HashMap<>(); + for (Map.Entry> nameToSupplier : buildTimeNamedPolicies.entrySet()) { + nameToPolicy.put(nameToSupplier.getKey(), nameToSupplier.getValue().get()); } - return Map.copyOf(result); - } - - private static void addPermissionToAction(Map cache, String role, String permissionToAction) { - final String permissionName; - final String action; - // incoming value is either in format perm1:action1 or perm1 (with or withot action) - if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { - // perm1:action1 - var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); - if (permToActions.length != 2) { - throw new ConfigurationException( - String.format("Invalid permission format '%s', please use exactly one permission to action separator", - permissionToAction)); - } - permissionName = permToActions[0].trim(); - action = permToActions[1].trim(); - } else { - // perm1 - permissionName = permissionToAction.trim(); - action = null; - } - - if (permissionName.isEmpty()) { - throw new ConfigurationException( - String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); - } - - cache.computeIfAbsent(permissionName, new Function() { - @Override - public PermissionToActions apply(String s) { - return new PermissionToActions(s); - } - }).addAction(action); + PathMatchingHttpSecurityPolicy.replaceNamedBuildTimePolicies(nameToPolicy); } public static abstract class DefaultAuthFailureHandler implements BiConsumer { @@ -380,10 +199,16 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(HttpConfiguration.class).get().auth.permissions.isEmpty(); + } } public static abstract class AbstractAuthenticationHandler implements Handler { volatile HttpAuthenticator authenticator; + volatile Boolean patchMatchingPolicyEnabled = null; final boolean proactiveAuthentication; public AbstractAuthenticationHandler(boolean proactiveAuthentication) { @@ -397,7 +222,12 @@ public void handle(RoutingContext event) { } //we put the authenticator into the routing context so it can be used by other systems event.put(HttpAuthenticator.class.getName(), authenticator); - setPathMatchingPolicy(event); + if (patchMatchingPolicyEnabled == null) { + setPatchMatchingPolicyEnabled(); + } + if (patchMatchingPolicyEnabled) { + setPathMatchingPolicy(event); + } //register the default auth failure handler if (proactiveAuthentication) { @@ -523,34 +353,14 @@ public void accept(SecurityIdentity identity, Throwable throwable, Boolean aBool } } - protected abstract void setPathMatchingPolicy(RoutingContext event); - } - - private static final class PermissionToActions { - private final String permissionName; - private final Set actions; - - private PermissionToActions(String permissionName) { - this.permissionName = permissionName; - this.actions = new HashSet<>(); - } - - private void addAction(String action) { - if (action != null) { - this.actions.add(action); + private synchronized void setPatchMatchingPolicyEnabled() { + if (patchMatchingPolicyEnabled == null) { + patchMatchingPolicyEnabled = !httpPermissionsEmpty(); } } - private Permission create(BiFunction permissionCreator) { - return permissionCreator.apply(permissionName, actions.toArray(new String[0])); - } - } + protected abstract void setPathMatchingPolicy(RoutingContext event); - private static Class loadClass(String className) { - try { - return Thread.currentThread().getContextClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); - } + protected abstract boolean httpPermissionsEmpty(); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java index fc0258841adf17..4a22ac63fa04e1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java @@ -1,12 +1,25 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.Map; + import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy { + + ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration runTimeConfig) { + super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, Map.of()); + } + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java index 2132ab9532dd35..c5624c4b6c7a4b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java @@ -1,13 +1,34 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.HashMap; +import java.util.Map; + import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton -public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy - implements HttpSecurityPolicy { +public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy implements HttpSecurityPolicy { + + // this map is planned for removal very soon as runtime named policies will make it obsolete + private static final Map HTTP_SECURITY_BUILD_TIME_POLICIES = new HashMap<>(); + + PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig) { + super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, + HTTP_SECURITY_BUILD_TIME_POLICIES); + } + + static synchronized void replaceNamedBuildTimePolicies(Map newPolicies) { + HTTP_SECURITY_BUILD_TIME_POLICIES.clear(); + HTTP_SECURITY_BUILD_TIME_POLICIES.putAll(newPolicies); + } + } diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java index 14a743cdac5564..c96ccd7b1ffc7b 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java @@ -43,9 +43,9 @@ public void testProcessor() throws Exception { consumerExists = typeAndDescriptionExist(consumers, "CHANNEL", "io.quarkus.test.devui.MyProcessor#channel"); } - JsonNode publisher = channel.get("publisher"); - if (publisher != null) { - publisherExists = typeAndDescriptionExist(publisher, "PROCESSOR", + JsonNode publishers = channel.get("publishers"); + if (publishers != null) { + publisherExists = typeAndDescriptionExist(publishers, "PROCESSOR", "io.quarkus.test.devui.MyProcessor#process()"); } } diff --git a/pom.xml b/pom.xml index f7323a0c9aa8d6..83366def81d3b8 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ 1.2.1 3.24.4 ${protoc.version} - 2.27.0 + 2.28.0 7.4.0