diff --git a/bom/application/pom.xml b/bom/application/pom.xml index dbeee6dbdb3e7..7f8a7bc00abae 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -56,7 +56,7 @@ 2.0 3.1.1 2.1.0 - 3.3.0 + 3.3.2 4.0.2 4.0.0 3.4.0 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java index 15eb300febff0..d826c15c68113 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java @@ -28,7 +28,7 @@ public abstract class NativeImageBuildContainerRunner extends NativeImageBuildRu protected NativeImageBuildContainerRunner(NativeConfig nativeConfig) { this.nativeConfig = nativeConfig; - containerRuntime = nativeConfig.containerRuntime().orElseGet(ContainerRuntimeUtil::detectContainerRuntime); + containerRuntime = ContainerRuntimeUtil.detectContainerRuntime(); this.baseContainerRuntimeArgs = new String[] { "--env", "LANG=C", "--rm" }; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java index c58500f1be332..c9275a0c79b7d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java @@ -104,8 +104,7 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig List extraArgs = nativeConfig.compression().additionalArgs().orElse(Collections.emptyList()); List commandLine = new ArrayList<>(); - ContainerRuntimeUtil.ContainerRuntime containerRuntime = nativeConfig.containerRuntime() - .orElseGet(ContainerRuntimeUtil::detectContainerRuntime); + ContainerRuntimeUtil.ContainerRuntime containerRuntime = ContainerRuntimeUtil.detectContainerRuntime(); commandLine.add(containerRuntime.getExecutableName()); commandLine.add("run"); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java index e34b1a0e1af54..73929157752bb 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java @@ -23,7 +23,7 @@ public class BuildAnalyticsConfig { /** * The Segment base URI. */ - @ConfigItem + @ConfigItem(name = "uri.base") public Optional uriBase; /** diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java new file mode 100644 index 0000000000000..9e0a6ae50578e --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.runtime; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.launch") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface LaunchConfig { + + /** + * If set to true, Quarkus will perform re-augmentation (assuming the {@code mutable-jar} package type is used) + */ + @WithDefault("false") + boolean rebuild(); +} diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index e17526e4c3390..9d42ce3af95b7 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -3,7 +3,7 @@ plugin-publish = "1.2.0" # updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail kotlin = "1.8.10" -smallrye-config = "3.3.0" +smallrye-config = "3.3.2" junit5 = "5.9.3" assertj = "3.24.2" diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index 848cd78bedcc9..b06e1b16173dc 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -1061,7 +1061,8 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap } // Add other properties that may be required for expansion - for (String value : effectiveProperties.values()) { + List effectivePropertyValues = new ArrayList<>(effectiveProperties.values()); + for (String value : effectivePropertyValues) { for (String reference : Expression.compile(value, LENIENT_SYNTAX, NO_TRIM).getReferencedStrings()) { String referenceValue = session.getUserProperties().getProperty(reference); if (referenceValue != null) { diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 12fa3b08dd033..8c8f8f6887217 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -110,6 +110,7 @@ quarkus.datasource.reactive.max-size=20 ---- <1> This configuration value is only required if there is more than one Reactive driver extension on the classpath. +[[configure-datasources]] == Configure datasources The following section describes the configuration for single or multiple datasources. @@ -431,7 +432,9 @@ You can override this by setting the `transactions` configuration property: * `quarkus.datasource.jdbc.transactions` for default unnamend datasource * `quarkus.datasource.__.jdbc.transactions` for named datasource -See the <> section below. +For more information, see the <> section below. + +To facilitate the storage of transaction logs in a database by using JDBC, see xref:transaction.adoc#jdbcstore[Configuring transaction logs to be stored in a datasource] section of the xref:transaction.adoc[Using transactions in Quarkus] guide. ==== Named datasources diff --git a/docs/src/main/asciidoc/transaction.adoc b/docs/src/main/asciidoc/transaction.adoc index bd870f4c95f25..4bbf0f4281e14 100644 --- a/docs/src/main/asciidoc/transaction.adoc +++ b/docs/src/main/asciidoc/transaction.adoc @@ -362,15 +362,36 @@ NOTE: The `event` object represents the transaction ID, and defines `toString()` TIP: In listener methods, you can access more information about the transaction in progress by accessing the `TransactionManager`, which is a CDI bean and can be ``@Inject``ed. -== Configuring transaction log to be stored in a DataSource +[[jdbcstore]] +== Configure storing of Quarkus transaction logs in a database -The Narayana project has the capability to store the transaction logs into a JDBC Datasource; this should be our recommendation for users needing transaction recovery capabilities, especially when running in volatile containers. +In cloud environments where persistent storage is not available, such as when application containers are unable to use persistent volumes, you can configure the transaction management to store transaction logs in a database by using a JDBC datasource. -To enable this capability, you need to set `quarkus.transaction-manager.object-store.type` to `jdbc` explicitly. Also, you can specify a datasource name to be used for the transaction log storage by setting `quarkus.transaction-manager.object-store.datasource`. It will use the default datasource configuration if not specified. +IMPORTANT: While there are several benefits to using a database to store transaction logs, you might notice a reduction in performance compared with using the file system to store the logs. -If you enable `quarkus.transaction-manager.object-store.create-table`, the transaction log table will be created automatically if it does not exist. +Quarkus allows the following JDBC-specific configuration of the object store included in `quarkus.transacion-manager.object-store.` properties, where can be: -NOTE: When enabling this capability, the transaction node identifier must be set through `quarkus.transaction-manager.node-name`. +* `type` (_string_): Configure this property to `jdbc` to enable usage of a Quarkus JDBC datasource for transaction logging. +The default value is `file-system`. +* `datasource` (_string_): Specify the name of the datasource for the transaction log storage. +If no value is provided for the `datasource` property, Quarkus uses the xref:datasource.adoc#configure-datasources[default datasource]. +* `create-table` (_boolean_): When set to `true`, the transaction log table gets automatically created if it does not already exist. +The default value is `false`. +* `drop-table` (_boolean_): When set to `true`, the tables are dropped on startup if they already exist. +The default value is `false`. +* `table-prefix` (string): Specify the prefix for a related table name. +The default value is `quarkus_`. + +[NOTE] +==== +To work around the current known issue of link:https://issues.redhat.com/browse/AG-209[Agroal having a different view on running transaction checks], set the datasource transaction type for the datasource responsible for writing the transaction logs to `disabled`: + +---- +quarkus.datasource.TX_LOG.jdbc.transactions=disabled +---- + +This example uses TX_LOG as the datasource name. +==== == Why always having a transaction manager? diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index e478e85c7992f..ac7cbb08cd51a 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -26,7 +26,7 @@ void testImageWithJava17() { Path path = getPath("openjdk-17-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime index d8be4fe7d3662..85a50ddf9aec9 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -1,5 +1,5 @@ # Use Java 17 base image -FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java index 9725522370569..106207774b677 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java @@ -16,7 +16,7 @@ public class JibConfig { /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17} + * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16} * is used as the default. * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.16} is used as the default. */ diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 9eabd8bb6daeb..afa60bb08ea23 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -90,7 +90,7 @@ public class JibProcessor { private static final IsClassPredicate IS_CLASS_PREDICATE = new IsClassPredicate(); private static final String BINARY_NAME_IN_CONTAINER = "application"; - private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17"; + private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16"; private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.16"; private static final String DEFAULT_BASE_IMAGE_USER = "185"; diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index eef8ad985c913..6c65a0c64e0a2 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -53,7 +53,6 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.xerial.snappy.OSInfo; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -95,6 +94,7 @@ import io.quarkus.kafka.client.runtime.KafkaBindingConverter; import io.quarkus.kafka.client.runtime.KafkaRecorder; import io.quarkus.kafka.client.runtime.KafkaRuntimeConfigProducer; +import io.quarkus.kafka.client.runtime.SnappyRecorder; import io.quarkus.kafka.client.runtime.ui.KafkaTopicClient; import io.quarkus.kafka.client.runtime.ui.KafkaUiRecorder; import io.quarkus.kafka.client.runtime.ui.KafkaUiUtils; @@ -292,7 +292,7 @@ public void build( } - @BuildStep(onlyIf = { IsSnappy.class, NativeOrNativeSourcesBuild.class }) + @BuildStep(onlyIf = { HasSnappy.class, NativeOrNativeSourcesBuild.class }) public void handleSnappyInNative(NativeImageRunnerBuildItem nativeImageRunner, BuildProducer reflectiveClass, BuildProducer nativeLibs) { @@ -307,19 +307,17 @@ public void handleSnappyInNative(NativeImageRunnerBuildItem nativeImageRunner, String path = root + dir + "/" + snappyNativeLibraryName; nativeLibs.produce(new NativeImageResourceBuildItem(path)); } else { // otherwise the native lib of the platform this build runs on - String dir = OSInfo.getNativeLibFolderPathForCurrentOS(); + String dir = SnappyUtils.getNativeLibFolderPathForCurrentOS(); String snappyNativeLibraryName = System.mapLibraryName("snappyjava"); String path = root + dir + "/" + snappyNativeLibraryName; nativeLibs.produce(new NativeImageResourceBuildItem(path)); } } - @BuildStep + @BuildStep(onlyIf = HasSnappy.class) @Record(ExecutionTime.RUNTIME_INIT) - void loadSnappyIfEnabled(KafkaRecorder recorder, KafkaBuildTimeConfig config) { - if (config.snappyEnabled) { - recorder.loadSnappy(); - } + void loadSnappyIfEnabled(SnappyRecorder recorder, KafkaBuildTimeConfig config) { + recorder.loadSnappy(); } @Consume(RuntimeConfigSetupCompleteBuildItem.class) @@ -585,17 +583,17 @@ public DevConsoleWebjarBuildItem setupWebJar(LaunchModeBuildItem launchModeBuild .build(); } - public static final class IsSnappy implements BooleanSupplier { + public static final class HasSnappy implements BooleanSupplier { private final KafkaBuildTimeConfig config; - public IsSnappy(KafkaBuildTimeConfig config) { + public HasSnappy(KafkaBuildTimeConfig config) { this.config = config; } @Override public boolean getAsBoolean() { - return config.snappyEnabled; + return QuarkusClassLoader.isClassPresentAtRuntime("org.xerial.snappy.OSInfo") && config.snappyEnabled; } } diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/SnappyUtils.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/SnappyUtils.java new file mode 100644 index 0000000000000..35a9060e72073 --- /dev/null +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/SnappyUtils.java @@ -0,0 +1,17 @@ +package io.quarkus.kafka.client.deployment; + +import org.xerial.snappy.OSInfo; + +/** + * This class should only be used if Snappy is available on the classpath. + */ +public class SnappyUtils { + + private SnappyUtils() { + // Avoid direct instantiation + } + + public static String getNativeLibFolderPathForCurrentOS() { + return OSInfo.getNativeLibFolderPathForCurrentOS(); + } +} diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java index 0de1b980d035b..8520c003c0170 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java @@ -1,69 +1,15 @@ package io.quarkus.kafka.client.runtime; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URL; import java.util.Optional; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; -import org.xerial.snappy.OSInfo; -import org.xerial.snappy.SnappyError; -import org.xerial.snappy.SnappyErrorCode; -import org.xerial.snappy.SnappyLoader; import io.quarkus.runtime.annotations.Recorder; @Recorder public class KafkaRecorder { - public void loadSnappy() { - // Resolve the library file name with a suffix (e.g., dll, .so, etc.) - String snappyNativeLibraryName = System.mapLibraryName("snappyjava"); - String snappyNativeLibraryPath = "/org/xerial/snappy/native/" + OSInfo.getNativeLibFolderPathForCurrentOS(); - boolean hasNativeLib = hasResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName); - - if (!hasNativeLib) { - String errorMessage = String.format("no native library is found for os.name=%s and os.arch=%s", OSInfo.getOSName(), - OSInfo.getArchName()); - throw new SnappyError(SnappyErrorCode.FAILED_TO_LOAD_NATIVE_LIBRARY, errorMessage); - } - - File out = extractLibraryFile( - SnappyLoader.class.getResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName), - snappyNativeLibraryName); - - System.load(out.getAbsolutePath()); - } - - private static boolean hasResource(String path) { - return SnappyLoader.class.getResource(path) != null; - } - - private static File extractLibraryFile(URL library, String name) { - String tmp = System.getProperty("java.io.tmpdir"); - File extractedLibFile = new File(tmp, name); - - try (BufferedInputStream inputStream = new BufferedInputStream(library.openStream()); - FileOutputStream fileOS = new FileOutputStream(extractedLibFile)) { - byte[] data = new byte[8192]; - int byteContent; - while ((byteContent = inputStream.read(data, 0, 8192)) != -1) { - fileOS.write(data, 0, byteContent); - } - } catch (IOException e) { - throw new UncheckedIOException( - "Unable to extract native library " + name + " to " + extractedLibFile.getAbsolutePath(), e); - } - - extractedLibFile.deleteOnExit(); - - return extractedLibFile; - } - public void checkBoostrapServers() { Config config = ConfigProvider.getConfig(); Boolean serviceBindingEnabled = config.getValue("quarkus.kubernetes-service-binding.enabled", Boolean.class); diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/SnappyRecorder.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/SnappyRecorder.java new file mode 100644 index 0000000000000..e06726204230e --- /dev/null +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/SnappyRecorder.java @@ -0,0 +1,61 @@ +package io.quarkus.kafka.client.runtime; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; + +import org.xerial.snappy.OSInfo; +import org.xerial.snappy.SnappyLoader; + +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class SnappyRecorder { + + public void loadSnappy() { + // Resolve the library file name with a suffix (e.g., dll, .so, etc.) + String snappyNativeLibraryName = System.mapLibraryName("snappyjava"); + String snappyNativeLibraryPath = "/org/xerial/snappy/native/" + OSInfo.getNativeLibFolderPathForCurrentOS(); + boolean hasNativeLib = hasResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName); + + if (!hasNativeLib) { + String errorMessage = String.format("no native library is found for os.name=%s and os.arch=%s", OSInfo.getOSName(), + OSInfo.getArchName()); + throw new RuntimeException(errorMessage); + } + + File out = extractLibraryFile( + SnappyLoader.class.getResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName), + snappyNativeLibraryName); + + System.load(out.getAbsolutePath()); + } + + private static boolean hasResource(String path) { + return SnappyLoader.class.getResource(path) != null; + } + + private static File extractLibraryFile(URL library, String name) { + String tmp = System.getProperty("java.io.tmpdir"); + File extractedLibFile = new File(tmp, name); + + try (BufferedInputStream inputStream = new BufferedInputStream(library.openStream()); + FileOutputStream fileOS = new FileOutputStream(extractedLibFile)) { + byte[] data = new byte[8192]; + int byteContent; + while ((byteContent = inputStream.read(data, 0, 8192)) != -1) { + fileOS.write(data, 0, byteContent); + } + } catch (IOException e) { + throw new UncheckedIOException( + "Unable to extract native library " + name + " to " + extractedLibFile.getAbsolutePath(), e); + } + + extractedLibFile.deleteOnExit(); + + return extractedLibFile; + } +} 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 35dcae0e411df..b9340ab472239 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 @@ -859,6 +859,30 @@ public enum ResponseMode { @ConfigItem(defaultValue = "true") public boolean allowMultipleCodeFlows = true; + /** + * Fail with the HTTP 401 error if the state cookie is present but no state query parameter is present. + *

+ * When either multiple authentications are disabled or the redirect URL + * matches the original request URL, the stale state cookie might remain in the browser cache from + * the earlier failed redirect to an OpenId Connect provider and be visible during the current request. + * For example, if Single-page application (SPA) uses XHR to handle redirects to the provider + * which does not support CORS for its authorization endpoint, the browser will block it + * and the state cookie created by Quarkus will remain in the browser cache. + * Quarkus will report an authentication failure when it will detect such an old state cookie but find no matching state + * query parameter. + *

+ * Reporting HTTP 401 error is usually the right thing to do in such cases, it will minimize a risk of the + * browser redirect loop but also can identify problems in the way SPA or Quarkus application manage redirects. + * For example, enabling {@link #javaScriptAutoRedirect} or having the provider redirect to URL configured + * 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 + * risk of browser redirect loops. + */ + @ConfigItem(defaultValue = "true") + public boolean failOnMissingStateParam = true; + /** * 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` diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 0104dc8dc68ed..33a546e4b2e3c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -106,7 +106,7 @@ public Uni apply(TenantConfigContext tenantContext) { @Override public Uni apply(MultiMap requestParams) { return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager, - requestParams); + requestParams, cookies); } }); } @@ -115,7 +115,7 @@ public Uni apply(MultiMap requestParams) { return Uni.createFrom().failure(new AuthenticationFailedException()); } else { return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager, - context.queryParams()); + context.queryParams(), cookies); } } @@ -136,7 +136,8 @@ private boolean isStateCookieAvailable(Map cookies) { } private Uni processRedirectFromOidc(RoutingContext context, OidcTenantConfig oidcTenantConfig, - IdentityProviderManager identityProviderManager, MultiMap requestParams) { + IdentityProviderManager identityProviderManager, MultiMap requestParams, + Map cookies) { // At this point it has already been detected that some state cookie is available. // If the state query parameter is not available or is available but no matching state cookie is found then if @@ -150,16 +151,14 @@ private Uni processRedirectFromOidc(RoutingContext context, Oi List stateQueryParam = requestParams.getAll(OidcConstants.CODE_FLOW_STATE); if (stateQueryParam.size() != 1) { - LOG.debug("State parameter can not be empty or multi-valued if the state cookie is present"); - return stateCookieIsMissing(oidcTenantConfig, context); + return stateParamIsMissing(oidcTenantConfig, context, cookies, stateQueryParam.size() > 1); } final Cookie stateCookie = context.request().getCookie( getStateCookieName(oidcTenantConfig) + "_" + stateQueryParam.get(0)); if (stateCookie == null) { - LOG.debug("Matching state cookie is not found"); - return stateCookieIsMissing(oidcTenantConfig, context); + return stateCookieIsMissing(oidcTenantConfig, context, cookies); } String[] parsedStateCookieValue = COOKIE_PATTERN.split(stateCookie.getValue()); @@ -239,14 +238,44 @@ public Uni apply(TenantConfigContext tenantContext) { } - private Uni stateCookieIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context) { + private Uni stateParamIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context, + Map cookies, boolean multipleStateQueryParams) { + if (multipleStateQueryParams) { + LOG.warn("State query parameter can not be multi-valued if the state cookie is present"); + removeStateCookies(oidcTenantConfig, context, cookies); + return Uni.createFrom().failure(new AuthenticationCompletionException()); + } + LOG.debug("State parameter can not be empty if the state cookie is present"); + return stateCookieIsNotMatched(oidcTenantConfig, context, cookies); + } + + private Uni stateCookieIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context, + Map cookies) { + LOG.debug("Matching state cookie is not found"); + return stateCookieIsNotMatched(oidcTenantConfig, context, cookies); + } + + private Uni stateCookieIsNotMatched(OidcTenantConfig oidcTenantConfig, RoutingContext context, + Map cookies) { if (!oidcTenantConfig.authentication.allowMultipleCodeFlows || context.request().path().equals(getRedirectPath(oidcTenantConfig, context))) { - return Uni.createFrom().failure(new AuthenticationCompletionException()); - } else { - context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE); - return Uni.createFrom().optional(Optional.empty()); + if (oidcTenantConfig.authentication.failOnMissingStateParam) { + return Uni.createFrom().failure(new AuthenticationCompletionException()); + } else { + removeStateCookies(oidcTenantConfig, context, cookies); + } + } + context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE); + return Uni.createFrom().optional(Optional.empty()); + } + + private void removeStateCookies(OidcTenantConfig oidcTenantConfig, RoutingContext context, Map cookies) { + for (String name : cookies.keySet()) { + if (name.startsWith(OidcUtils.STATE_COOKIE_NAME)) { + OidcUtils.removeCookie(context, oidcTenantConfig, name); + } } + } private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParams, OidcTenantConfig oidcConfig) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java index 96021b564068d..df1d6de283c35 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java @@ -127,10 +127,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti if (!accepts.isEmpty()) { boolean hasAtLeastOneMatch = false; for (int i = 0; i < accepts.size(); i++) { - boolean matches = acceptHeaderMatches(target, accepts.get(i)); - if (matches) { - hasAtLeastOneMatch = true; - break; + try { + boolean matches = acceptHeaderMatches(target, accepts.get(i)); + if (matches) { + hasAtLeastOneMatch = true; + break; + } + } catch (IllegalArgumentException ignored) { + // the provided header was not valid } } if (!hasAtLeastOneMatch) { @@ -150,6 +154,10 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti } } + /** + * @return {@code true} if the provided string matches one of the {@code @Produces} values of the resource method + * @throws IllegalArgumentException if the provided string cannot be parsed into a {@link MediaType} + */ private boolean acceptHeaderMatches(RequestMapper.RequestMatch target, String accepts) { if ((accepts != null) && !accepts.equals(MediaType.WILDCARD)) { int commaIndex = accepts.indexOf(','); @@ -157,9 +165,8 @@ private boolean acceptHeaderMatches(RequestMapper.RequestMatch MediaType[] producesMediaTypes = target.value.getProduces().getSortedOriginalMediaTypes(); if (!multipleAcceptsValues && (producesMediaTypes.length == 1)) { // the point of this branch is to eliminate any list creation or string indexing as none is needed - MediaType acceptsMediaType = MediaType.valueOf(accepts.trim()); MediaType providedMediaType = producesMediaTypes[0]; - return providedMediaType.isCompatible(acceptsMediaType); + return providedMediaType.isCompatible(toMediaType(accepts.trim())); } else if (multipleAcceptsValues && (producesMediaTypes.length == 1)) { // this is fairly common case, so we want it to be as fast as possible // we do that by manually splitting the accepts header and immediately checking diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/mediatype/InvalidAcceptTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/mediatype/InvalidAcceptTest.java new file mode 100644 index 0000000000000..507e8c4d6bfa9 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/mediatype/InvalidAcceptTest.java @@ -0,0 +1,50 @@ +package org.jboss.resteasy.reactive.server.vertx.test.mediatype; + +import static io.restassured.RestAssured.config; +import static io.restassured.RestAssured.given; +import static io.restassured.config.EncoderConfig.encoderConfig; + +import java.util.function.Supplier; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.restassured.http.ContentType; + +public class InvalidAcceptTest { + + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloResource.class); + } + }); + + @Test + public void test() { + given().config(config().encoderConfig(encoderConfig().encodeContentTypeAs("invalid", ContentType.TEXT))).body("dummy") + .accept("invalid").get("/hello") + .then() + .statusCode(406); + } + + @Path("hello") + public static class HelloResource { + + @Produces("text/plain") + @GET + public String hello() { + return "hello"; + } + } +} diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index ef7d0fa28bd28..2f7bfe6302863 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -109,6 +109,7 @@ quarkus.oidc.tenant-https.authentication.error-path=/tenant-https/error quarkus.oidc.tenant-https.authentication.pkce-required=true quarkus.oidc.tenant-https.authentication.pkce-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU quarkus.oidc.tenant-https.authentication.cookie-same-site=strict +quarkus.oidc.tenant-https.authentication.fail-on-missing-state-param=false quarkus.oidc.tenant-javascript.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-javascript.client-id=quarkus-app diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 085b1ac532f88..40e2da377d0d0 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -227,6 +227,56 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception { } } + @Test + public void testStateCookieIsPresentButStateParamNot() throws Exception { + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(false); + + WebResponse webResponse = webClient + .loadWebResponse( + new WebRequest(URI.create("http://localhost:8081/tenant-https").toURL())); + String keycloakUrl = webResponse.getResponseHeaderValue("location"); + verifyLocationHeader(webClient, keycloakUrl, "tenant-https_test", "tenant-https", + true); + + HtmlPage page = webClient.getPage(keycloakUrl); + + assertEquals("Sign in to quarkus", page.getTitleText()); + HtmlForm loginForm = page.getForms().get(0); + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); + webResponse = loginForm.getInputByName("login").click().getWebResponse(); + webClient.getOptions().setThrowExceptionOnFailingStatusCode(true); + + // This is a redirect from the OIDC server to the endpoint containing the state and code + String endpointLocation = webResponse.getResponseHeaderValue("location"); + assertTrue(endpointLocation.startsWith("https")); + endpointLocation = "http" + endpointLocation.substring(5); + + // State cookie is present + Cookie stateCookie = getStateCookie(webClient, "tenant-https_test"); + assertNull(stateCookie.getSameSite()); + verifyCodeVerifier(stateCookie, keycloakUrl); + + // Make a call without an extra state query param, status is 401 + webResponse = webClient.loadWebResponse(new WebRequest(URI.create(endpointLocation + "&state=123").toURL())); + assertEquals(401, webResponse.getStatusCode()); + + // Make a call without the state query param, confirm the old state cookie is removed, status is 302 + webResponse = webClient.loadWebResponse(new WebRequest(URI.create("http://localhost:8081/tenant-https").toURL())); + assertEquals(302, webResponse.getStatusCode()); + // the old state cookie has been removed + assertNull(webClient.getCookieManager().getCookie(stateCookie.getName())); + // new state cookie is created + Cookie newStateCookie = getStateCookie(webClient, "tenant-https_test"); + assertNotEquals(newStateCookie.getName(), stateCookie.getName()); + + webClient.getCookieManager().clearCookies(); + } + } + @Test public void testCodeFlowForceHttpsRedirectUriWithQueryAndPkce() throws Exception { try (final WebClient webClient = createWebClient()) {