diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index e6b2da300498e..c7a4dc0a10737 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -315,13 +315,14 @@ jobs: elif [ "${GIB_IMPACTED_MODULES}" != '_all_' ] then # Important: keep -pl ... in actual jobs in sync with the following grep commands! - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -qPv 'integration-tests/(devtools|gradle|maven|devmode|kubernetes/.*)|tcks/.*'); then run_jvm=false; fi - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -q 'integration-tests/devtools'); then run_devtools=false; fi - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -q 'integration-tests/gradle'); then run_gradle=false; fi - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -qP 'integration-tests/(maven|devmode)'); then run_maven=false; fi - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -qP 'integration-tests/kubernetes/.*'); then run_kubernetes=false; fi - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -qPv '(docs|integration-tests|tcks)/.*'); then run_quickstarts=false; fi - if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -q 'tcks/.*'); then run_tcks=false; fi + # do not use grep -q as it can exit before echo has finished outputting and cause broken pipes + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -Pv 'integration-tests/(devtools|gradle|maven|devmode|kubernetes/.*)|tcks/.*' > /dev/null); then run_jvm=false; fi + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep 'integration-tests/devtools' > /dev/null); then run_devtools=false; fi + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep 'integration-tests/gradle' > /dev/null); then run_gradle=false; fi + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -P 'integration-tests/(maven|devmode)' > /dev/null); then run_maven=false; fi + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -P 'integration-tests/kubernetes/.*' > /dev/null); then run_kubernetes=false; fi + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep -Pv '(docs|integration-tests|tcks)/.*' > /dev/null); then run_quickstarts=false; fi + if ! (echo -n "${GIB_IMPACTED_MODULES}" | grep 'tcks/.*' > /dev/null); then run_tcks=false; fi fi echo "run_jvm=${run_jvm}, run_devtools=${run_devtools}, run_gradle=${run_gradle}, run_maven=${run_maven}, run_kubernetes=${run_kubernetes}, run_quickstarts=${run_quickstarts}, run_tcks=${run_tcks}" echo "run_jvm=${run_jvm}" >> $GITHUB_OUTPUT diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java index 7e45c652dbcf4..cbf5e6e62ee96 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java @@ -74,7 +74,8 @@ public void beforeTest(Test task) { Map props = task.getSystemProperties(); ApplicationModel appModel = getApplicationModel(TEST); - SmallRyeConfig config = buildEffectiveConfiguration(appModel.getAppArtifact()).getConfig(); + SmallRyeConfig config = buildEffectiveConfiguration(appModel) + .getConfig(); config.getOptionalValue(TEST.getProfileKey(), String.class) .ifPresent(value -> props.put(TEST.getProfileKey(), value)); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java index 58976d97f65a9..81301d6c807ad 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java @@ -27,6 +27,7 @@ import org.gradle.api.tasks.SourceSet; import org.gradle.process.JavaForkOptions; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.gradle.dsl.Manifest; import io.quarkus.maven.dependency.ResolvedDependency; import io.smallrye.common.expression.Expression; @@ -80,7 +81,7 @@ private BaseConfig buildBaseConfig() { // Used to handle the (deprecated) buildNative and testNative tasks. project.getExtensions().getExtraProperties().getProperties().forEach((k, v) -> { - if (k.startsWith("quarkus.")) { + if (k.startsWith("quarkus.") || k.startsWith("platform.quarkus.")) { forcedPropertiesProperty.put(k, v.toString()); } }); @@ -117,7 +118,9 @@ protected Manifest manifest() { return baseConfig().manifest(); } - protected EffectiveConfig buildEffectiveConfiguration(ResolvedDependency appArtifact) { + protected EffectiveConfig buildEffectiveConfiguration(ApplicationModel appModel) { + ResolvedDependency appArtifact = appModel.getAppArtifact(); + Map properties = new HashMap<>(); exportCustomManifestProperties(properties); @@ -126,7 +129,7 @@ protected EffectiveConfig buildEffectiveConfiguration(ResolvedDependency appArti // Used to handle the (deprecated) buildNative and testNative tasks. project.getExtensions().getExtraProperties().getProperties().forEach((k, v) -> { - if (k.startsWith("quarkus.")) { + if (k.startsWith("quarkus.") || k.startsWith("platform.quarkus.")) { forcedPropertiesProperty.put(k, v.toString()); } }); @@ -140,6 +143,7 @@ protected EffectiveConfig buildEffectiveConfiguration(ResolvedDependency appArti defaultProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion()); return EffectiveConfig.builder() + .withPlatformProperties(appModel.getPlatformProperties()) .withForcedProperties(forcedPropertiesProperty.get()) .withTaskProperties(properties) .withBuildProperties(quarkusBuildProperties.get()) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java index 4833c5be53da2..da99d0a7386b3 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java @@ -91,7 +91,7 @@ public Deploy() { public void checkRequiredExtensions() { ApplicationModel appModel = resolveAppModelForBuild(); Properties sysProps = new Properties(); - sysProps.putAll(extension().buildEffectiveConfiguration(appModel.getAppArtifact()).getValues()); + sysProps.putAll(extension().buildEffectiveConfiguration(appModel).getValues()); try (CuratedApplication curatedApplication = QuarkusBootstrap.builder() .setBaseClassLoader(getClass().getClassLoader()) .setExistingModel(appModel) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java index 52583a3088096..8e49f6a2b5a10 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java @@ -60,6 +60,15 @@ private EffectiveConfig(Builder builder) { // 100 -> microprofile.properties in classpath (provided by default sources) // 0 -> fallback config source for error workaround (see below) + PropertiesConfigSource platformPropertiesConfigSource; + if (builder.platformProperties.isEmpty()) { + // we don't have the model yet so we don't have the Platform properties around + platformPropertiesConfigSource = new PropertiesConfigSource( + Map.of("platform.quarkus.native.builder-image", "<>"), "platformProperties", 0); + } else { + platformPropertiesConfigSource = new PropertiesConfigSource(builder.platformProperties, "platformProperties", 0); + } + this.config = ConfigUtils.emptyConfigBuilder() .forClassLoader(toUrlClassloader(builder.sourceDirectories)) .withSources(new PropertiesConfigSource(builder.forcedProperties, "forcedProperties", 600)) @@ -70,9 +79,7 @@ private EffectiveConfig(Builder builder) { .withSources(new YamlConfigSourceLoader.InFileSystem()) .withSources(new YamlConfigSourceLoader.InClassPath()) .addPropertiesSources() - // todo: this is due to ApplicationModel#getPlatformProperties not being included in the effective config - .withSources(new PropertiesConfigSource(Map.of("platform.quarkus.native.builder-image", "<>"), - "NativeConfig#builderImage", 0)) + .withSources(platformPropertiesConfigSource) .withDefaultValues(builder.defaultProperties) .withProfile(builder.profile) .withMapping(PackageConfig.class) @@ -122,6 +129,7 @@ static Builder builder() { } static final class Builder { + private Map platformProperties = emptyMap(); private Map forcedProperties = emptyMap(); private Map taskProperties = emptyMap(); private Map buildProperties = emptyMap(); @@ -134,6 +142,11 @@ EffectiveConfig build() { return new EffectiveConfig(this); } + Builder withPlatformProperties(Map platformProperties) { + this.platformProperties = platformProperties; + return this; + } + Builder withForcedProperties(Map forcedProperties) { this.forcedProperties = forcedProperties; return this; diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java index 40e811c1b02a2..7885ce37c0d1d 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildDependencies.java @@ -144,7 +144,8 @@ private void jarDependencies(Path libBoot, Path libMain) { } ApplicationModel appModel = resolveAppModelForBuild(); - SmallRyeConfig config = getExtensionView().buildEffectiveConfiguration(appModel.getAppArtifact(), new HashMap<>()) + SmallRyeConfig config = getExtensionView() + .buildEffectiveConfiguration(appModel, new HashMap<>()) .getConfig(); // see https://quarkus.io/guides/class-loading-reference#configuring-class-loading diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index f5f75c86681ed..39eae2b51e20b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -244,13 +244,16 @@ void generateBuild() { ApplicationModel appModel = resolveAppModelForBuild(); SmallRyeConfig config = getExtensionView() - .buildEffectiveConfiguration(appModel.getAppArtifact(), getAdditionalForcedProperties().get().getProperties()) + .buildEffectiveConfiguration(appModel, getAdditionalForcedProperties().get().getProperties()) .getConfig(); Map quarkusProperties = Expressions.withoutExpansion(() -> { Map values = new HashMap<>(); for (String key : config.getMapKeys("quarkus").values()) { values.put(key, config.getConfigValue(key).getValue()); } + for (String key : config.getMapKeys("platform.quarkus").values()) { + values.put(key, config.getConfigValue(key).getValue()); + } return values; }); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java index a86e1b4e794f9..226a1cccacc19 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java @@ -119,7 +119,7 @@ public Set getInputDirectory() { public void generateCode() throws IOException { ApplicationModel appModel = ToolingUtils.deserializeAppModel(getApplicationModel().get().getAsFile().toPath()); Map configMap = getExtensionView() - .buildEffectiveConfiguration(appModel.getAppArtifact(), new HashMap<>()).getValues(); + .buildEffectiveConfiguration(appModel, new HashMap<>()).getValues(); File outputPath = getGeneratedOutputDirectory().get().getAsFile(); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java index 9dc97a6c53ea7..22bb019e69da6 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java @@ -38,6 +38,7 @@ import org.gradle.process.JavaForkOptions; import org.gradle.util.GradleVersion; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.gradle.QuarkusPlugin; import io.quarkus.gradle.dsl.Manifest; @@ -209,8 +210,10 @@ private void exportCustomManifestProperties(Map properties) { } } - protected EffectiveConfig buildEffectiveConfiguration(ResolvedDependency appArtifact, + protected EffectiveConfig buildEffectiveConfiguration(ApplicationModel appModel, Map additionalForcedProperties) { + ResolvedDependency appArtifact = appModel.getAppArtifact(); + Map properties = new HashMap<>(); exportCustomManifestProperties(properties); @@ -235,6 +238,7 @@ protected EffectiveConfig buildEffectiveConfiguration(ResolvedDependency appArti forced.put("quarkus.native.enabled", "true"); } return EffectiveConfig.builder() + .withPlatformProperties(appModel.getPlatformProperties()) .withForcedProperties(forced) .withTaskProperties(properties) .withBuildProperties(getQuarkusBuildProperties().get()) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java index e65e7e8c2759b..f944cbd1cd6ba 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java @@ -102,7 +102,7 @@ public void setJvmArgs(List jvmArgs) { public void runQuarkus() { ApplicationModel appModel = resolveAppModelForBuild(); Properties sysProps = new Properties(); - sysProps.putAll(extension().buildEffectiveConfiguration(appModel.getAppArtifact()).getValues()); + sysProps.putAll(extension().buildEffectiveConfiguration(appModel).getValues()); try (CuratedApplication curatedApplication = QuarkusBootstrap.builder() .setBaseClassLoader(getClass().getClassLoader()) .setExistingModel(appModel) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java index 39882f3e5434c..6d5af8ef63caa 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java @@ -49,7 +49,7 @@ public void dumpEffectiveConfiguration() { try { ApplicationModel appModel = resolveAppModelForBuild(); EffectiveConfig effectiveConfig = getExtensionView() - .buildEffectiveConfiguration(appModel.getAppArtifact(), + .buildEffectiveConfiguration(appModel, getAdditionalForcedProperties().get().getProperties()); SmallRyeConfig config = effectiveConfig.getConfig(); List sourceNames = new ArrayList<>(); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java index 17b4822d9e923..11060269f5e54 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java @@ -19,7 +19,7 @@ import io.quarkus.utilities.OS; public abstract class QuarkusTask extends DefaultTask { - private static final List WORKER_BUILD_FORK_OPTIONS = List.of("quarkus."); + private static final List WORKER_BUILD_FORK_OPTIONS = List.of("quarkus.", "platform.quarkus."); private final transient QuarkusPluginExtension extension; protected final File projectDir; diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index f10a3a66eee15..cd714fcf13433 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -333,6 +333,7 @@ public class EagerAppBean { NOTE: Quarkus users are encouraged to always prefer the `@Observes StartupEvent` to `@Initialized(ApplicationScoped.class)` as explained in the xref:lifecycle.adoc[Application Initialization and Termination] guide. +[[request-context-lifecycle]] === Request Context Lifecycle The request context is also active: diff --git a/docs/src/main/asciidoc/kotlin.adoc b/docs/src/main/asciidoc/kotlin.adoc index 767e610cf8af8..65b0a83a26ff5 100644 --- a/docs/src/main/asciidoc/kotlin.adoc +++ b/docs/src/main/asciidoc/kotlin.adoc @@ -221,8 +221,8 @@ group = '...' // set your group version = '1.0.0-SNAPSHOT' java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } allOpen { // <2> @@ -233,12 +233,12 @@ allOpen { // <2> } compileKotlin { - kotlinOptions.jvmTarget = JavaVersion.VERSION_11 + kotlinOptions.jvmTarget = JavaVersion.VERSION_21 kotlinOptions.javaParameters = true } compileTestKotlin { - kotlinOptions.jvmTarget = JavaVersion.VERSION_11 + kotlinOptions.jvmTarget = JavaVersion.VERSION_21 } ---- @@ -290,8 +290,8 @@ group = '...' // set your group version = "1.0.0-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } allOpen { // <2> diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index 30fe4e03adb6a..dd33ede5af1b5 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -123,9 +123,9 @@ public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { public Uni checkPermission(RoutingContext event, Uni identity, AuthorizationRequestContext requestContext) { if (customRequestAuthorization(event)) { - return Uni.createFrom().item(CheckResult.PERMIT); + return CheckResult.permit(); } - return Uni.createFrom().item(CheckResult.DENY); + return CheckResult.deny(); } @Override @@ -182,6 +182,17 @@ You can also create global `HttpSecurityPolicy` invoked on every request. Just do not implement the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method and leave the policy nameless. ==== +[[policy-active-cdi-request-context]] +=== Inject `@RequestScoped` beans into `HttpSecurityPolicy` + +`@RequestScoped` beans can only be injected when the xref:cdi-reference.adoc#request-context-lifecycle[CDI request context] is active. +The context can be activated by users, for example with the `@ActivateRequestContext`, however authorization happens before Quarkus prepares some `@RequestScoped` beans. +We recommend to let Quarkus activate and prepare CDI request context for you. +For example, consider a situation where you want to inject a bean from the Jakarta REST context, such as the `jakarta.ws.rs.core.UriInfo` bean. +In this case, you must apply the `HttpSecurityPolicy` to Jakarta REST endpoints. This can be achieved in one of the following ways: +* Use the `@AuthorizationPolicy` security annotation. +* Set the `quarkus.http.auth.permission.custom1.applies-to=jaxrs` configuration property. + === Matching on paths and methods Permission sets can also specify paths and methods as a comma-separated list. @@ -494,7 +505,7 @@ s| `@PermitAll` | Specifies that all security roles are allowed to invoke the sp s| `@RolesAllowed` | Specifies the list of security roles allowed to access methods in an application. s| `@Authenticated` | {project-name} provides the `io.quarkus.security.Authenticated` annotation that permits any authenticated user to access the resource. It's equivalent to `@RolesAllowed("**")`. s| `@PermissionsAllowed` | Specifies the list of permissions that are allowed to invoke the specified methods. -s| `@AuthorizationPolicy` | Specifies named `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy` that should authorize access to the specified endpoints.HttpSecurityPolicy. +s| `@AuthorizationPolicy` | Specifies named `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy` that should authorize access to the specified Jakarta REST endpoints. Named HttpSecurityPolicy can be used for general authorization checks as demonstrated by <>. |=== diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index b42ce570911ed..620e644fd5e2d 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -242,6 +242,8 @@ You can enforce the order by implementing a default `SecurityIdentityAugmentor#p By default, the request context is not activated when augmenting the security identity, this means that if you want to use for example Hibernate that mandates a request context, you will have a `jakarta.enterprise.context.ContextNotActiveException`. +IMPORTANT: Please also review the xref:security-proactive-authentication.adoc#cdi-request-context-activation[Activating the CDI request context] section of the "Proactive authentication" guide. + The solution is to activate the request context, the following example shows how to get the roles from an Hibernate with Panache `UserRoleEntity`. [source,java] diff --git a/docs/src/main/asciidoc/security-getting-started-tutorial.adoc b/docs/src/main/asciidoc/security-getting-started-tutorial.adoc index 6b07d7c9c1f9c..72fee1f4e9401 100644 --- a/docs/src/main/asciidoc/security-getting-started-tutorial.adoc +++ b/docs/src/main/asciidoc/security-getting-started-tutorial.adoc @@ -324,13 +324,16 @@ For example: ---- quarkus.http.auth.basic=true -quarkus.datasource.db-kind=postgresql -quarkus.datasource.username=quarkus -quarkus.datasource.password=quarkus -quarkus.datasource.jdbc.url=jdbc:postgresql:security_jpa +%prod.quarkus.datasource.db-kind=postgresql +%prod.quarkus.datasource.username=quarkus +%prod.quarkus.datasource.password=quarkus +%prod.quarkus.datasource.jdbc.url=jdbc:postgresql:security_jpa quarkus.hibernate-orm.database.generation=drop-and-create ---- + +By adding the `%prod.` profile prefix, you ensure that the data source properties are only observed by an application running in production mode. + ==== + . To initialize the database with users and roles, implement the `Startup` class, as outlined in the following code snippet: @@ -384,7 +387,7 @@ In a production environment, do not store plain text passwords. As a result, the `quarkus-security-jpa` defaults to using bcrypt-hashed passwords. ==== -== Test your application by using Dev Services for PostgreSQL +== Test your application in dev mode by using Dev Services for PostgreSQL Complete the integration testing of your application in JVM and native modes by using xref:dev-services.adoc#databases[Dev Services for PostgreSQL] before you run your application in production mode. @@ -411,21 +414,8 @@ To run your application in dev mode: include::{includes}/devtools/dev.adoc[] -The following properties configuration demonstrates how to enable PostgreSQL testing to run only in production (`prod`) mode. In this scenario, `Dev Services for PostgreSQL` launches and configures a `PostgreSQL` test container. -[source,properties] ----- -%prod.quarkus.datasource.db-kind=postgresql -%prod.quarkus.datasource.username=quarkus -%prod.quarkus.datasource.password=quarkus -%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost/quarkus - -quarkus.hibernate-orm.database.generation=drop-and-create ----- - -If you add the `%prod.` profile prefix, data source properties are not visible to `Dev Services for PostgreSQL` and are only observed by an application running in production mode. - To write the integration test, use the following code sample: [source,java] @@ -503,7 +493,7 @@ While developing your application, you can add and run tests individually by usi Dev Services for PostgreSQL supports testing while you develop by providing a separate PostgreSQL test container that does not conflict with the dev mode container. ==== -== Test your application using Curl or browser +== Test your application in production mode by using Curl or browser To test your application using Curl or the browser, you must first start a PostgreSQL server, then compile and run your application either in JVM or native mode. @@ -513,7 +503,7 @@ To test your application using Curl or the browser, you must first start a Postg ---- docker run --rm=true --name security-getting-started -e POSTGRES_USER=quarkus \ -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=quarkus \ - -p 5432:5432 postgres:14.1 + -p 5432:5432 postgres:17 ---- === Compile and run the application @@ -576,11 +566,7 @@ public $ curl -i -X GET http://localhost:8080/api/admin HTTP/1.1 401 Unauthorized -Content-Length: 14 -Content-Type: text/html;charset=UTF-8 WWW-Authenticate: Basic - -Not authorized ---- ==== * Connect to a protected endpoint as an authorized user: @@ -616,10 +602,6 @@ If a resource is protected with `@RolesAllowed("user")`, the user `admin` is not $ curl -i -X GET -u admin:admin http://localhost:8080/api/users/me HTTP/1.1 403 Forbidden -Content-Length: 34 -Content-Type: text/html;charset=UTF-8 - -Forbidden ---- Finally, the user named `user` is authorized, and the security context contains the principal details, for example, the username. diff --git a/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc index e5fe4efd02415..36df439faad74 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc @@ -9,6 +9,9 @@ include::_attributes.adoc[] :categories: security :topics: security,oidc,client :extensions: io.quarkus:quarkus-oidc-client-registration +:extension-status: experimental + +include::{includes}/extension-status.adoc[] Typically, you have to register an OIDC client (application) manually in your OIDC provider's dashboard. During this process, you specify the human readable application name, allowed redirect and post logout URLs, and other properties. diff --git a/docs/src/main/asciidoc/security-proactive-authentication.adoc b/docs/src/main/asciidoc/security-proactive-authentication.adoc index de4be3fb47cf9..424f3e5ff0721 100644 --- a/docs/src/main/asciidoc/security-proactive-authentication.adoc +++ b/docs/src/main/asciidoc/security-proactive-authentication.adoc @@ -18,6 +18,7 @@ Gain practical insights and strategies for various application scenarios. Proactive authentication is enabled in Quarkus by default. It ensures that all incoming requests with credentials are authenticated, even if the target page does not require authentication. As a result, requests with invalid credentials are rejected, even if the target page is public. +Requests without credentials are not rejected, because anonymous requests are allowed. You can turn off this default behavior if you want to authenticate only when the target page requires it. To turn off proactive authentication so that authentication occurs only when the target page requires it, modify the `application.properties` configuration file as follows: @@ -30,7 +31,7 @@ quarkus.http.auth.proactive=false If you turn off proactive authentication, the authentication process runs only when an identity is requested. An identity can be requested because of security rules that require the user to authenticate or because programmatic access to the current identity is required. -If proactive authentication is used, accessing `SecurityIdentity` is a blocking operation. +If proactive authentication is not used, accessing `SecurityIdentity` is a blocking operation. This is because authentication might have yet to happen, and accessing `SecurityIdentity` might require calls to external systems, such as databases, that might block the operation. For blocking applications, this is not an issue. However, if you have disabled authentication in a reactive application, this fails because you cannot do blocking operations on the I/O thread. @@ -96,6 +97,19 @@ public class HelloService { } ---- +[[cdi-request-context-activation]] +== Activating the CDI request context + +You may need to inject `@RequestScoped` beans during authentication and authorization. +A good example of this is accessing a database during a `SecurityIdentity` augmentation, +which is described in the xref:security-customization.adoc#security-identity-customization[Security Identity Customization] section of the "Security Tips and Tricks" guide. +If authentication or authorization fails with the `jakarta.enterprise.context.ContextNotActiveException`, disabling proactive authentication is most often the best solution. +Users can also activate xref:cdi-reference.adoc#request-context-lifecycle[CDI request context], for example, by using the `@ActivateRequestContext` annotation. +However, some CDI beans may not be ready for use. + +One exception to this solution is a situation when application endpoints are secured with the xref:security-authorize-web-endpoints-reference.adoc#authorization-using-configuration[Authorization using configuration]. +For more information, see the xref:security-authorize-web-endpoints-reference.adoc#policy-active-cdi-request-context[Inject RequestScoped beans into HttpSecurityPolicy] section of the "Authorization of Web endpoints" guide for more information. + [[customize-auth-exception-responses]] == Customize authentication exception responses diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/logs/OtelLoggingTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/logs/OtelLoggingTest.java index 7099ab11bf565..e5c07c0eb9a9d 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/logs/OtelLoggingTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/logs/OtelLoggingTest.java @@ -172,6 +172,80 @@ public void testException() { .doesNotContainKey("sampled")); } + @Test + public void testLogFormatingData() { + final String message = "Replacement string"; + final String expected = "infof " + message; + assertEquals("hello " + message, jBossLoggingBean.helloLogFormating(message)); + + List finishedLogRecordItems = logRecordExporter.getFinishedLogRecordItemsAtLeast(1); + LogRecordData last = finishedLogRecordItems.get(finishedLogRecordItems.size() - 1); + + assertThat(last.getSpanContext().getSpanId()).isEqualTo("0000000000000000"); + assertThat(last.getSpanContext().getTraceId()).isEqualTo("00000000000000000000000000000000"); + assertThat(last.getSpanContext().getTraceFlags().asHex()).isEqualTo("00"); + assertThat(last.getTimestampEpochNanos()).isNotNull().isLessThan(System.currentTimeMillis() * 1_000_000); + + assertThat(last) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasBody(expected) + .hasAttributesSatisfying( + attributes -> assertThat(attributes) + .containsEntry(CODE_NAMESPACE.getKey(), + "io.quarkus.opentelemetry.deployment.logs.OtelLoggingTest$JBossLoggingBean") + .containsEntry(CODE_FUNCTION.getKey(), "helloLogFormating") + .containsEntry(THREAD_NAME.getKey(), Thread.currentThread().getName()) + .containsEntry(THREAD_ID.getKey(), Thread.currentThread().getId()) + .containsEntry("log.logger.namespace", "org.jboss.logging.Logger") + .containsKey(CODE_LINENO.getKey()) + .doesNotContainKey(EXCEPTION_TYPE) + .doesNotContainKey(EXCEPTION_MESSAGE) + .doesNotContainKey(EXCEPTION_STACKTRACE) + .doesNotContainKey(LOG_FILE_PATH) + // attributed do not duplicate tracing data + .doesNotContainKey("spanId") + .doesNotContainKey("traceId") + .doesNotContainKey("sampled")); + } + + @Test + public void testLogParameterValue() { + final String message = "Replacement parameter value"; + final String expected = "infov " + message; + assertEquals("hello " + message, jBossLoggingBean.helloLogParameterValue(message)); + + List finishedLogRecordItems = logRecordExporter.getFinishedLogRecordItemsAtLeast(1); + LogRecordData last = finishedLogRecordItems.get(finishedLogRecordItems.size() - 1); + + assertThat(last.getSpanContext().getSpanId()).isEqualTo("0000000000000000"); + assertThat(last.getSpanContext().getTraceId()).isEqualTo("00000000000000000000000000000000"); + assertThat(last.getSpanContext().getTraceFlags().asHex()).isEqualTo("00"); + assertThat(last.getTimestampEpochNanos()).isNotNull().isLessThan(System.currentTimeMillis() * 1_000_000); + + assertThat(last) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasBody(expected) + .hasAttributesSatisfying( + attributes -> assertThat(attributes) + .containsEntry(CODE_NAMESPACE.getKey(), + "io.quarkus.opentelemetry.deployment.logs.OtelLoggingTest$JBossLoggingBean") + .containsEntry(CODE_FUNCTION.getKey(), "helloLogParameterValue") + .containsEntry(THREAD_NAME.getKey(), Thread.currentThread().getName()) + .containsEntry(THREAD_ID.getKey(), Thread.currentThread().getId()) + .containsEntry("log.logger.namespace", "org.jboss.logging.Logger") + .containsKey(CODE_LINENO.getKey()) + .doesNotContainKey(EXCEPTION_TYPE) + .doesNotContainKey(EXCEPTION_MESSAGE) + .doesNotContainKey(EXCEPTION_STACKTRACE) + .doesNotContainKey(LOG_FILE_PATH) + // attributed do not duplicate tracing data + .doesNotContainKey("spanId") + .doesNotContainKey("traceId") + .doesNotContainKey("sampled")); + } + private String extractStackTrace(final Throwable throwable) { try (StringWriter sw = new StringWriter(1024); PrintWriter pw = new PrintWriter(sw)) { throwable.printStackTrace(pw); @@ -201,5 +275,15 @@ public boolean logException(final Throwable throwable) { LOG.error("logging an exception", throwable); return true; } + + public String helloLogFormating(final String replacement) { + LOG.infof("infof %s", replacement); + return "hello " + replacement; + } + + public String helloLogParameterValue(final String replacement) { + LOG.infov("infov {0}", replacement); + return "hello " + replacement; + } } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java index 65e2de1270e70..382ea4af501e8 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java @@ -13,6 +13,7 @@ import java.time.Instant; import java.util.Map; import java.util.Optional; +import java.util.logging.Formatter; import java.util.logging.Level; import org.eclipse.microprofile.config.Config; @@ -52,7 +53,15 @@ protected void doPublish(ExtLogRecord record) { } if (record.getMessage() != null) { - logRecordBuilder.setBody(record.getMessage()); + // Get the message + final Formatter formatter = getFormatter(); + String logMsg; + if (formatter != null) { + logMsg = formatter.format(record); + } else { + logMsg = record.getFormattedMessage(); + } + logRecordBuilder.setBody(logMsg); } final AttributesBuilder attributes = Attributes.builder(); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingSummary.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingSummary.java index ee61127ca9751..815dac30b4cfb 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingSummary.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingSummary.java @@ -37,7 +37,7 @@ public long getPendingCount() { /** * Gets the lowest message id that was not yet acknowledged. * - * @return the lowest message id + * @return the lowest message id; may be {@code null} */ public String getLowestId() { return lowestId; @@ -46,7 +46,7 @@ public String getLowestId() { /** * Gets the highest message id that was not yet acknowledged. * - * @return the highest message id + * @return the highest message id; may be {@code null} */ public String getHighestId() { return highestId; @@ -56,7 +56,7 @@ public String getHighestId() { * Get the list of every consumer in the consumer group with at least one pending message, * and the number of pending messages it has. * - * @return the map composed of consumer -> number of message + * @return the map composed of consumer -> number of message; may be empty */ public Map getConsumers() { return consumers; diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java index 3d33cc56a2e8e..cb96a3da49d7d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java @@ -66,7 +66,7 @@ public XTrimArgs limit(long limit) { public List toArgs() { List args = new ArrayList<>(); - if (maxlen > 0) { + if (maxlen >= 0) { if (minid != null) { throw new IllegalArgumentException("Cannot use `MAXLEN` and `MINID` together"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java index ca566c31b7dde..4ccba2e69cea0 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java @@ -370,12 +370,14 @@ protected XPendingSummary decodeAsXPendingSummary(Response r) { } var pending = r.get(0).toLong(); - var lowest = r.get(1).toString(); - var highest = r.get(2).toString(); + var lowest = r.get(1) != null ? r.get(1).toString() : null; + var highest = r.get(2) != null ? r.get(2).toString() : null; Map consumers = new HashMap<>(); - for (Response nested : r.get(3)) { - consumers.put(nested.get(0).toString(), nested.get(1).toLong()); + if (r.get(3) != null) { + for (Response nested : r.get(3)) { + consumers.put(nested.get(0).toString(), nested.get(1).toLong()); + } } return new XPendingSummary(pending, lowest, highest, consumers); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java index 5958c37f56d51..d491f69587c12 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java @@ -648,11 +648,22 @@ void xGroupSetIdWithArgs() { @Test void xPendingSummaryTest() { Map payload = Map.of("sensor-id", 1234, "temperature", 19); + + stream.xadd(key, payload); + stream.xtrim(key, new XTrimArgs().maxlen(0)); + + stream.xgroupCreate(key, "my-group", "0-0"); + + XPendingSummary summaryEmpty = stream.xpending(key, "my-group"); + assertThat(summaryEmpty.getPendingCount()).isEqualTo(0); + assertThat(summaryEmpty.getHighestId()).isNull(); + assertThat(summaryEmpty.getLowestId()).isNull(); + assertThat(summaryEmpty.getConsumers()).isEmpty(); + for (int i = 0; i < 100; i++) { stream.xadd(key, payload); } - stream.xgroupCreate(key, "my-group", "0-0"); List> messages = stream.xreadgroup("my-group", "consumer-123", key, ">"); assertThat(messages).hasSize(100); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java index df4d62e61a01c..dec431322a6fd 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java @@ -18,6 +18,7 @@ import io.quarkus.redis.datasource.stream.XAddArgs; import io.quarkus.redis.datasource.stream.XPendingArgs; import io.quarkus.redis.datasource.stream.XPendingSummary; +import io.quarkus.redis.datasource.stream.XTrimArgs; import io.quarkus.redis.datasource.transactions.TransactionResult; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @@ -48,31 +49,45 @@ public void streamBlocking() { TransactionalStreamCommands stream = tx.stream(String.class); assertThat(stream.getDataSource()).isEqualTo(tx); + stream.xadd(key, payload); + stream.xtrim(key, new XTrimArgs().maxlen(0)); + + stream.xgroupCreate(key, "g1", "0"); + + stream.xpending(key, "g1"); + stream.xadd(key, payload); stream.xadd(key, new XAddArgs().nomkstream(), payload); stream.xread(key, "0"); // 3 -> 2 messages - stream.xgroupCreate(key, "g1", "0"); stream.xreadgroup("g1", "c1", key, ">"); stream.xpending(key, "g1"); stream.xpending(key, "g1", StreamRange.of("-", "+"), 10, new XPendingArgs().consumer("c1")); }); - assertThat(result.size()).isEqualTo(7); + assertThat(result.size()).isEqualTo(10); assertThat(result.discarded()).isFalse(); + assertThat((String) result.get(0)).isNotBlank(); - assertThat((String) result.get(1)).isNotBlank(); + assertThat((Long) result.get(1)).isEqualTo(1); - String id1 = result.get(0); - String id2 = result.get(1); + assertThat(result. get(3).getPendingCount()).isEqualTo(0); + assertThat(result. get(3).getHighestId()).isNull(); + assertThat(result. get(3).getLowestId()).isNull(); + assertThat(result. get(3).getConsumers()).isEmpty(); - assertThat((List>) result.get(2)).hasSize(2); - assertThat((List>) result.get(4)).hasSize(2); + String id1 = result.get(4); + String id2 = result.get(5); - assertThat(((XPendingSummary) result.get(5)).getPendingCount()).isEqualTo(2); - List list = result.get(6); + assertThat((List>) result.get(6)).hasSize(2); + assertThat((List>) result.get(7)).hasSize(2); - assertThat(((List) result.get(6))).hasSize(2); + assertThat((result. get(8)).getPendingCount()).isEqualTo(2); + assertThat((result. get(8)).getHighestId()).isNotNull(); + assertThat((result. get(8)).getLowestId()).isNotNull(); + assertThat((result. get(8)).getConsumers()).hasSize(1); + List list = result.get(9); + assertThat(list).hasSize(2); List ids = list.stream().map(PendingMessage::getMessageId).collect(Collectors.toList()); assertThat(ids).containsExactly(id1, id2); } @@ -84,29 +99,41 @@ public void streamReactive() { assertThat(stream.getDataSource()).isEqualTo(tx); return stream.xadd(key, payload) - .chain((x) -> stream.xadd(key, new XAddArgs().nomkstream(), payload)) - .chain(x -> stream.xread(key, "0")) + .chain(x -> stream.xtrim(key, new XTrimArgs().maxlen(0))) .chain(x -> stream.xgroupCreate(key, "g1", "0")) + .chain(x -> stream.xpending(key, "g1")) + .chain(x -> stream.xadd(key, payload)) + .chain(x -> stream.xadd(key, new XAddArgs().nomkstream(), payload)) + .chain(x -> stream.xread(key, "0")) .chain(x -> stream.xreadgroup("g1", "c1", key, ">")) .chain(x -> stream.xpending(key, "g1")) .chain(x -> stream.xpending(key, "g1", StreamRange.of("-", "+"), 10)); }).await().indefinitely(); - assertThat(result.size()).isEqualTo(7); + assertThat(result.size()).isEqualTo(10); assertThat(result.discarded()).isFalse(); + assertThat((String) result.get(0)).isNotBlank(); - assertThat((String) result.get(1)).isNotBlank(); + assertThat((Long) result.get(1)).isEqualTo(1); + + assertThat(result. get(3).getPendingCount()).isEqualTo(0); + assertThat(result. get(3).getHighestId()).isNull(); + assertThat(result. get(3).getLowestId()).isNull(); + assertThat(result. get(3).getConsumers()).isEmpty(); - String id1 = result.get(0); - String id2 = result.get(1); + String id1 = result.get(4); + String id2 = result.get(5); - assertThat((List>) result.get(2)).hasSize(2); - assertThat((List>) result.get(4)).hasSize(2); + assertThat((List>) result.get(6)).hasSize(2); + assertThat((List>) result.get(7)).hasSize(2); - assertThat(((XPendingSummary) result.get(5)).getPendingCount()).isEqualTo(2); - List list = result.get(6); + assertThat((result. get(8)).getPendingCount()).isEqualTo(2); + assertThat((result. get(8)).getHighestId()).isNotNull(); + assertThat((result. get(8)).getLowestId()).isNotNull(); + assertThat((result. get(8)).getConsumers()).hasSize(1); - assertThat(((List) result.get(6))).hasSize(2); + List list = result.get(9); + assertThat(list).hasSize(2); List ids = list.stream().map(PendingMessage::getMessageId).collect(Collectors.toList()); assertThat(ids).containsExactly(id1, id2); } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java index 7ed5887381a76..41a74ca33ffb0 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java @@ -94,7 +94,9 @@ public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) // FQN -> Quoted Config Key -> Quoted Simple Name -> Simple Name quarkusFallbacks.put(quotedFullName, quotedConfigKey); quarkusFallbacks.put(quotedConfigKey, restClient.getSimpleName()); - quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + if (!quotedSimpleName.equals(quotedFullName)) { + quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + } fullNameRelocates.add(quotedConfigKey); fullNameRelocates.add(restClient.getSimpleName()); fullNameRelocates.add(quotedSimpleName); @@ -103,7 +105,9 @@ public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) quarkusFallbacks.put(quotedFullName, configKey); quarkusFallbacks.put(configKey, quotedConfigKey); quarkusFallbacks.put(quotedConfigKey, restClient.getSimpleName()); - quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + if (!quotedSimpleName.equals(quotedFullName)) { + quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + } fullNameRelocates.add(configKey); fullNameRelocates.add(quotedConfigKey); fullNameRelocates.add(restClient.getSimpleName()); @@ -112,14 +116,18 @@ public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) } else { // FQN -> Quoted Simple Name -> Simple Name quarkusFallbacks.put(quotedFullName, restClient.getSimpleName()); - quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + if (!quotedSimpleName.equals(quotedFullName)) { + quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + } fullNameRelocates.add(restClient.getSimpleName()); fullNameRelocates.add(quotedSimpleName); } } else { // FQN -> Quoted Simple Name -> Simple Name quarkusFallbacks.put(quotedFullName, restClient.getSimpleName()); - quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + if (!quotedSimpleName.equals(quotedFullName)) { + quarkusFallbacks.put(restClient.getSimpleName(), quotedSimpleName); + } fullNameRelocates.add(restClient.getSimpleName()); fullNameRelocates.add(quotedSimpleName); } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java index 7b73ed5ae3942..7e596d0a3fd20 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java @@ -324,6 +324,29 @@ void buildTimeConfig() { assertFalse(buildTimeConfig.clients().get(ConfigKeyRestClient.class.getName()).removesTrailingSlash()); } + @Test + void defaultPackage() { + RegisteredRestClient registeredRestClient = new RegisteredRestClient("FullNameRestClient", "FullNameRestClient", null); + // application.properties in test/resources + SmallRyeConfig config = ConfigUtils.emptyConfigBuilder() + .withMapping(RestClientsConfig.class) + .withCustomizers(new SmallRyeConfigBuilderCustomizer() { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + new AbstractRestClientConfigBuilder() { + @Override + public List getRestClients() { + return List.of(registeredRestClient); + } + }.configBuilder(builder); + } + }) + .build(); + + RestClientsConfig restClientsConfig = config.getConfigMapping(RestClientsConfig.class); + assertEquals(1, restClientsConfig.clients().size()); + } + private void verifyConfig(RestClientConfig config) { assertTrue(config.url().isPresent()); assertThat(config.url().get()).isEqualTo("http://localhost:8080"); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerMtlsPKCS12CertificateReloadTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerMtlsPKCS12CertificateReloadTest.java new file mode 100644 index 0000000000000..520ffec23b8b1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerMtlsPKCS12CertificateReloadTest.java @@ -0,0 +1,174 @@ +package io.quarkus.vertx.http.certReload; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.security.cert.X509Certificate; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.net.ssl.SSLHandshakeException; + +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.vertx.http.runtime.options.TlsCertificateReloader; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.net.PfxOptions; +import io.vertx.ext.web.Router; + +@Certificates(baseDir = "target/certificates", certificates = { + @Certificate(name = "mtls-reload-A", formats = Format.PKCS12, password = "password", client = true), + @Certificate(name = "mtls-reload-B", formats = Format.PKCS12, password = "password", client = true, duration = 365), +}) +@DisabledOnOs(OS.WINDOWS) +public class MainHttpServerMtlsPKCS12CertificateReloadTest { + + @TestHTTPResource(value = "/hello", ssl = true) + URL url; + + public static final File temp = new File("target/test-certificates-" + UUID.randomUUID()); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(MyBean.class)) + // .overrideConfigKey("quarkus.http.insecure-requests", "redirect") + .overrideConfigKey("quarkus.http.ssl.certificate.reload-period", "30s") + .overrideConfigKey("quarkus.http.ssl.certificate.key-store-file", temp.getAbsolutePath() + "/server-keystore.p12") + .overrideConfigKey("quarkus.http.ssl.certificate.key-store-password", "password") + .overrideConfigKey("quarkus.http.ssl.certificate.trust-store-file", + temp.getAbsolutePath() + "/server-truststore.p12") + .overrideConfigKey("quarkus.http.ssl.certificate.trust-store-password", "password") + .overrideConfigKey("quarkus.http.ssl.client-auth=required", "required") + + .overrideConfigKey("loc", temp.getAbsolutePath()) + .setBeforeAllCustomizer(() -> { + try { + // Prepare a random directory to store the certificates. + temp.mkdirs(); + Files.copy(new File("target/certificates/mtls-reload-A-keystore.p12").toPath(), + new File(temp, "/server-keystore.p12").toPath()); + Files.copy(new File("target/certificates/mtls-reload-A-server-truststore.p12").toPath(), + new File(temp, "/server-truststore.p12").toPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .setAfterAllCustomizer(() -> { + try { + Files.deleteIfExists(new File(temp, "/server-keystore.p12").toPath()); + Files.deleteIfExists(new File(temp, "/server-truststore.p12").toPath()); + Files.deleteIfExists(temp.toPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + @Inject + Vertx vertx; + + @ConfigProperty(name = "loc") + File certs; + + @Test + void test() throws IOException, ExecutionException, InterruptedException, TimeoutException { + var options = new HttpClientOptions() + .setSsl(true) + .setDefaultPort(url.getPort()) + .setDefaultHost(url.getHost()) + .setKeyCertOptions( + new PfxOptions().setPath("target/certificates/mtls-reload-A-client-keystore.p12") + .setPassword("password")) + .setTrustOptions( + new PfxOptions().setPath("target/certificates/mtls-reload-A-client-truststore.p12") + .setPassword("password")); + + String response1 = vertx.createHttpClient(options) + .request(HttpMethod.GET, "/hello") + .flatMap(HttpClientRequest::send) + .flatMap(HttpClientResponse::body) + .map(Buffer::toString) + .toCompletionStage().toCompletableFuture().join(); + + // Update certs + Files.copy(new File("target/certificates/mtls-reload-B-keystore.p12").toPath(), + new File(certs, "/server-keystore.p12").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + Files.copy(new File("target/certificates/mtls-reload-B-server-truststore.p12").toPath(), + new File(certs, "/server-truststore.p12").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + // Trigger the reload + TlsCertificateReloader.reload().toCompletableFuture().get(10, TimeUnit.SECONDS); + + // The client keystore and truststore are not updated, thus it should fail. + assertThatThrownBy(() -> vertx.createHttpClient(options) + .request(HttpMethod.GET, "/hello") + .flatMap(HttpClientRequest::send) + .flatMap(HttpClientResponse::body) + .map(Buffer::toString) + .toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class); + + var options2 = new HttpClientOptions(options) + .setKeyCertOptions( + new PfxOptions().setPath("target/certificates/mtls-reload-B-client-keystore.p12") + .setPassword("password")) + .setTrustOptions( + new PfxOptions().setPath("target/certificates/mtls-reload-B-client-truststore.p12") + .setPassword("password")); + + var response2 = vertx.createHttpClient(options2) + .request(HttpMethod.GET, "/hello") + .flatMap(HttpClientRequest::send) + .flatMap(HttpClientResponse::body) + .map(Buffer::toString) + .toCompletionStage().toCompletableFuture().join(); + + assertThat(response1).isNotEqualTo(response2); // Because cert duration are different. + + // Trigger another reload + TlsCertificateReloader.reload().toCompletableFuture().get(10, TimeUnit.SECONDS); + + var response3 = vertx.createHttpClient(options2) + .request(HttpMethod.GET, "/hello") + .flatMap(HttpClientRequest::send) + .flatMap(HttpClientResponse::body) + .map(Buffer::toString) + .toCompletionStage().toCompletableFuture().join(); + + assertThat(response2).isEqualTo(response3); + } + + public static class MyBean { + + public void onStart(@Observes Router router) { + router.get("/hello").handler(rc -> { + var exp = ((X509Certificate) rc.request().connection().sslSession().getLocalCertificates()[0]).getNotAfter() + .toInstant().toEpochMilli(); + rc.response().end("Hello " + exp); + }); + } + + } + +} diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/echarts/echarts-horizontal-stacked-bar.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/echarts/echarts-horizontal-stacked-bar.js index 7c5f286a01931..bbe38d10b0d5e 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/echarts/echarts-horizontal-stacked-bar.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/echarts/echarts-horizontal-stacked-bar.js @@ -45,7 +45,7 @@ class EchartsHorizontalStackedBar extends EchartsAbstractCanvas { const sectionTitlesArray = this.sectionTitles.split(','); const sectionValuesArray = this.sectionValues.split(','); - + const option = new Object(); // Tooltip option.tooltip = new Object(); @@ -53,7 +53,7 @@ class EchartsHorizontalStackedBar extends EchartsAbstractCanvas { // Legend option.legend = new Object(); option.legend.show = false; - + // Grid option.grid = new Object(); option.grid.left = '3%'; @@ -86,7 +86,7 @@ class EchartsHorizontalStackedBar extends EchartsAbstractCanvas { serie.name = title; serie.type = 'bar'; serie.stack = 'total'; - serie.data = [value], + serie.data = [value]; serie.color = color; option.series.push(serie); } @@ -95,4 +95,4 @@ class EchartsHorizontalStackedBar extends EchartsAbstractCanvas { } } -customElements.define('echarts-horizontal-stacked-bar', EchartsHorizontalStackedBar); \ No newline at end of file +customElements.define('echarts-horizontal-stacked-bar', EchartsHorizontalStackedBar); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java index e7cfbf4aa50cc..e7f85fece611e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java @@ -34,5 +34,6 @@ public class FilterConfig { /** * Order in which this path config is applied. Higher priority takes precedence */ + @ConfigItem public OptionalInt order; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java index b89526740d05b..cfda33146a47c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java @@ -212,7 +212,7 @@ private static SSLOptions reloadFileContent(SSLOptions ssl, ServerSslConfig conf } if (configuration.certificate.trustStoreFile.isPresent()) { - var opts = ((KeyStoreOptions) copy.getKeyCertOptions()); + var opts = ((KeyStoreOptions) copy.getTrustOptions()); opts.setValue(Buffer.buffer(getFileContent(configuration.certificate.trustStoreFile.get()))); copy.setTrustOptions(opts); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 8bfcc874f444c..290134c5a752b 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -286,7 +286,7 @@ public ClientImpl build() { options.setMaxChunkSize(maxChunkSize); return new ClientImpl(options, - configuration, + new ConfigurationImpl(configuration), CLIENT_CONTEXT_RESOLVER.resolve(Thread.currentThread().getContextClassLoader()), null, null, diff --git a/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderTest.java b/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderTest.java new file mode 100644 index 0000000000000..9590ce428e750 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderTest.java @@ -0,0 +1,46 @@ +package org.jboss.resteasy.reactive.client.impl; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.Test; + +public class ClientBuilderTest { + + @Test + void configurationCopiedOnBuild() { + ClientBuilder builder = ClientBuilder.newBuilder(); + + Client firstClient = builder.build(); + firstClient.property("foo", "bar"); + firstClient.register(Always500.class); + + Client secondClient = builder.build(); + + try (firstClient; secondClient) { + // Should not be the same instance + assertNotSame(firstClient.getConfiguration(), secondClient.getConfiguration()); + + // Should not be affected by changes to firstClient's Configuration + assertNull(secondClient.getConfiguration().getProperty("foo")); + assertFalse(secondClient.getConfiguration().getClasses().contains(Always500.class)); + } + } + + public static class Always500 implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.serverError().build()); + } + } +} diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 63cd689794698..eb771150e494b 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -46,7 +46,7 @@ 4.1.0 3.2.3 - 1.14.11 + 1.15.11 5.10.5 3.9.9 3.26.3 diff --git a/pom.xml b/pom.xml index d11d783da143e..b98e87431ec1a 100644 --- a/pom.xml +++ b/pom.xml @@ -71,11 +71,11 @@ 0.8.12 6.13.4 5.5.0 - 6.6.4.Final + 6.6.5.Final 4.13.0 - 1.14.18 + 1.15.11 7.0.3.Final - 2.4.3.Final + 2.4.4.Final 8.0.2.Final 7.2.2.Final