diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 72fa010268d4bb..074b48e9d93781 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -20,10 +20,10 @@ 1.0.16 5.0.0 3.0.2 - 3.1.3 + 3.1.5 1.3.2 1 - 1.1.2 + 1.1.4 2.1.4.Final 3.0.2.Final 6.2.5.Final @@ -878,6 +878,16 @@ quarkus-oidc-token-propagation-deployment ${project.version} + + io.quarkus + quarkus-oidc-db-token-state-manager + ${project.version} + + + io.quarkus + quarkus-oidc-db-token-state-manager-deployment + ${project.version} + io.quarkus quarkus-oidc-token-propagation-reactive diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 1fc318536f9e0f..7cf80cdf2f9d4a 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -33,7 +33,7 @@ ${version.surefire.plugin} - 3.1.3 + 3.1.5 1.0.0 2.5.10 @@ -114,7 +114,7 @@ 7.3.0 - 2.30.0 + 2.31.0 @@ -802,7 +802,7 @@ org.apache.groovy groovy - 4.0.13 + 4.0.15 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java index 4d0fd6495051ce..5f3d5d46246d1f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -150,4 +150,9 @@ public interface Capability { String CACHE = QUARKUS_PREFIX + ".cache"; String JDBC_ORACLE = QUARKUS_PREFIX + ".jdbc.oracle"; + String REACTIVE_PG_CLIENT = QUARKUS_PREFIX + ".reactive-pg-client"; + String REACTIVE_ORACLE_CLIENT = QUARKUS_PREFIX + ".reactive-oracle-client"; + String REACTIVE_MYSQL_CLIENT = QUARKUS_PREFIX + ".reactive-mysql-client"; + String REACTIVE_MSSQL_CLIENT = QUARKUS_PREFIX + ".reactive-mssql-client"; + String REACTIVE_DB2_CLIENT = QUARKUS_PREFIX + ".reactive-db2-client"; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java index 712630ce95a9c4..e0bacc8780aedb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -62,10 +63,16 @@ static Version parse(List lines) { String vendorVersion = secondMatcher.group(VENDOR_VERSION_GROUP); String buildInfo = secondMatcher.group(BUILD_INFO_GROUP); - String graalVersion = graalVersion(buildInfo); + String graalVersion = graalVersion(buildInfo, v.feature()); + if (vendorVersion.contains("-dev")) { + graalVersion = graalVersion + "-dev"; + } String mandrelVersion = mandrelVersion(vendorVersion); - Distribution dist = isMandrel(vendorVersion) ? Distribution.MANDREL : Distribution.ORACLE; + Distribution dist = isMandrel(vendorVersion) ? Distribution.MANDREL : Distribution.GRAALVM; String versNum = (dist == Distribution.MANDREL ? mandrelVersion : graalVersion); + if (versNum == null) { + return UNKNOWN_VERSION; + } return new Version(lines.stream().collect(Collectors.joining("\n")), versNum, v.feature(), v.update(), dist); } else { @@ -100,7 +107,7 @@ private static String matchVersion(String version) { return null; } - private static String graalVersion(String buildInfo) { + private static String graalVersion(String buildInfo, int jdkFeature) { if (buildInfo == null) { return null; } @@ -109,10 +116,23 @@ private static String graalVersion(String buildInfo) { return null; } String version = buildInfo.substring(idx + JVMCI_BUILD_PREFIX.length()); - return matchVersion(version); + Matcher versMatcher = VERSION_PATTERN.matcher(version); + if (versMatcher.find()) { + return matchVersion(version); + } else { + return GRAAL_MAPPING.get(jdkFeature); + } } } + // Temporarily work around https://github.com/quarkusio/quarkus/issues/36246, + // till we have a consensus on how to move forward in + // https://github.com/quarkusio/quarkus/issues/34161 + private static final Map GRAAL_MAPPING = Map.of(22, "24.0", + 23, "24.1", + 24, "25.0", + 25, "25.1"); + public static final class Version implements Comparable { /** @@ -127,12 +147,12 @@ public static final class Version implements Comparable { "(GraalVM|native-image)( Version)? " + VersionParseHelper.VERS_FORMAT + "(?.*?)?" + "(\\(Java Version (?[0-9]+)(\\.(?[0-9]*)\\.(?[0-9]*))?.*)?$"); - static final Version VERSION_21_3 = new Version("GraalVM 21.3", "21.3", Distribution.ORACLE); - static final Version VERSION_21_3_0 = new Version("GraalVM 21.3.0", "21.3.0", Distribution.ORACLE); - public static final Version VERSION_22_3_0 = new Version("GraalVM 22.3.0", "22.3.0", Distribution.ORACLE); - public static final Version VERSION_22_2_0 = new Version("GraalVM 22.2.0", "22.2.0", Distribution.ORACLE); - public static final Version VERSION_23_0_0 = new Version("GraalVM 23.0.0", "23.0.0", Distribution.ORACLE); - public static final Version VERSION_23_1_0 = new Version("GraalVM 23.1.0", "23.1.0", Distribution.ORACLE); + static final Version VERSION_21_3 = new Version("GraalVM 21.3", "21.3", Distribution.GRAALVM); + static final Version VERSION_21_3_0 = new Version("GraalVM 21.3.0", "21.3.0", Distribution.GRAALVM); + public static final Version VERSION_22_3_0 = new Version("GraalVM 22.3.0", "22.3.0", Distribution.GRAALVM); + public static final Version VERSION_22_2_0 = new Version("GraalVM 22.2.0", "22.2.0", Distribution.GRAALVM); + public static final Version VERSION_23_0_0 = new Version("GraalVM 23.0.0", "23.0.0", Distribution.GRAALVM); + public static final Version VERSION_23_1_0 = new Version("GraalVM 23.1.0", "23.1.0", Distribution.GRAALVM); public static final Version MINIMUM = VERSION_22_2_0; public static final Version CURRENT = VERSION_23_0_0; @@ -226,7 +246,10 @@ public static Version of(Stream output) { if (lines.size() == 3) { // Attempt to parse the new 3-line version scheme first. - return VersionParseHelper.parse(lines); + Version parsedVersion = VersionParseHelper.parse(lines); + if (parsedVersion != VersionParseHelper.UNKNOWN_VERSION) { + return parsedVersion; + } } else if (lines.size() == 1) { // Old, single line version parsing logic final String line = lines.get(0); @@ -249,7 +272,7 @@ public static Version of(Stream output) { version, jFeature, jUpdate, - isMandrel(distro) ? Distribution.MANDREL : Distribution.ORACLE); + isMandrel(distro) ? Distribution.MANDREL : Distribution.GRAALVM); } } @@ -290,7 +313,7 @@ public boolean isJava17() { } enum Distribution { - ORACLE, + GRAALVM, MANDREL; } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 6429997c3223c9..ad06af41577620 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -459,9 +459,10 @@ private RuntimeException imageGenerationFailed(int exitValue, boolean isContaine } private void checkGraalVMVersion(GraalVM.Version version) { - log.info("Running Quarkus native-image plugin on " + version.getFullVersion()); + log.info("Running Quarkus native-image plugin on " + version.distribution.name() + " " + version.getVersionAsString() + + " JDK " + version.javaFeatureVersion + "." + version.javaUpdateVersion); if (version.isObsolete()) { - throw new IllegalStateException("Out of date version of GraalVM detected: " + version.getFullVersion() + "." + throw new IllegalStateException("Out of date version of GraalVM detected: " + version.getVersionAsString() + "." + " Quarkus currently supports " + GraalVM.Version.CURRENT.getVersionAsString() + ". Please upgrade GraalVM to this version."); } 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 c9275a0c79b7da..80b05436996a82 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 @@ -51,6 +51,10 @@ public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativ if (!runUpxFromHost(upxPathFromSystem.get(), image.getPath().toFile(), nativeConfig)) { throw new IllegalStateException("Unable to compress the native executable"); } + } else if (nativeConfig.remoteContainerBuild()) { + log.errorf("Compression of native executables is not yet implemented for remote container builds."); + throw new IllegalStateException( + "Unable to compress the native executable: Compression of native executables is not yet supported for remote container builds"); } else if (nativeImageRunner.isContainerBuild()) { log.infof("Running UPX from a container using the builder image: " + effectiveBuilderImage); if (!runUpxInContainer(image, nativeConfig, effectiveBuilderImage)) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 743582f056a7d6..c935c2ba45bf49 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -40,6 +40,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.ConfigMappingBuildItem; @@ -69,6 +70,7 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.paths.PathCollection; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.annotations.StaticInitSafe; import io.quarkus.runtime.configuration.ConfigBuilder; @@ -267,8 +269,11 @@ void generateConfigClass( List runTimeConfigBuilders) throws IOException { - reportUnknownBuildProperties(launchModeBuildItem.getLaunchMode(), - configItem.getReadResult().getUnknownBuildProperties()); + // So it only reports during the build, because it is very likely that the property is available in runtime + // and, it will be caught by the RuntimeConfig and log double warnings + if (!launchModeBuildItem.getLaunchMode().isDevOrTest()) { + ConfigDiagnostic.unknownProperties(configItem.getReadResult().getUnknownBuildProperties()); + } if (liveReloadBuildItem.isLiveReload()) { return; @@ -320,14 +325,6 @@ void generateConfigClass( .run(); } - private static void reportUnknownBuildProperties(LaunchMode launchMode, Set unknownBuildProperties) { - // So it only reports during the build, because it is very likely that the property is available in runtime - // and, it will be caught by the RuntimeConfig and log double warnings - if (!launchMode.isDevOrTest()) { - ConfigDiagnostic.unknownProperties(unknownBuildProperties); - } - } - @BuildStep public void suppressNonRuntimeConfigChanged( BuildProducer suppressNonRuntimeConfigChanged) { @@ -441,6 +438,31 @@ public void watchConfigFiles(BuildProducer wa } } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void unknownConfigFiles( + ApplicationArchivesBuildItem applicationArchives, + LaunchModeBuildItem launchModeBuildItem, + ConfigRecorder configRecorder) throws Exception { + + PathCollection rootDirectories = applicationArchives.getRootArchive().getRootDirectories(); + if (!rootDirectories.isSinglePath()) { + return; + } + + Set buildTimeFiles = new HashSet<>(); + buildTimeFiles.addAll(ConfigDiagnostic.configFiles(rootDirectories.getSinglePath())); + buildTimeFiles.addAll(ConfigDiagnostic.configFilesFromLocations()); + + // Report always at build time since config folder and locations may differ from build to runtime + ConfigDiagnostic.unknownConfigFiles(buildTimeFiles); + + // No need to include the application files, because they don't change + if (!launchModeBuildItem.getLaunchMode().isDevOrTest()) { + configRecorder.unknownConfigFiles(); + } + } + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) @Record(ExecutionTime.RUNTIME_INIT) void warnDifferentProfileUsedBetweenBuildAndRunTime(ConfigRecorder configRecorder) { diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java index d8ee390aa4ff07..9d251f8a48de0b 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java @@ -1,7 +1,7 @@ package io.quarkus.deployment.pkg.steps; +import static io.quarkus.deployment.pkg.steps.GraalVM.Distribution.GRAALVM; import static io.quarkus.deployment.pkg.steps.GraalVM.Distribution.MANDREL; -import static io.quarkus.deployment.pkg.steps.GraalVM.Distribution.ORACLE; import static org.assertj.core.api.Assertions.assertThat; import java.util.stream.Stream; @@ -19,20 +19,20 @@ public class GraalVMTest { @Test public void testGraalVMVersionDetected() { // Version detection after: https://github.com/oracle/graal/pull/6302 (3 lines of version output) - assertVersion(new Version("GraalVM 23.0.0", "23.0.0", ORACLE), MANDREL, + assertVersion(new Version("GraalVM 23.0.0", "23.0.0", GRAALVM), MANDREL, Version.of(Stream.of(("native-image 17.0.6 2023-01-17\n" + "OpenJDK Runtime Environment Mandrel-23.0.0-dev (build 17.0.6+10)\n" + "OpenJDK 64-Bit Server VM Mandrel-23.0.0-dev (build 17.0.6+10, mixed mode)").split("\\n")))); - assertVersion(new Version("GraalVM 23.0.0", "23.0.0", ORACLE), MANDREL, + assertVersion(new Version("GraalVM 23.0.0", "23.0.0", GRAALVM), MANDREL, Version.of(Stream.of(("native-image 17.0.6 2023-01-17\n" + "GraalVM Runtime Environment Mandrel-23.0.0-dev (build 17.0.6+10)\n" + "Substrate VM Mandrel-23.0.0-dev (build 17.0.6+10, serial gc)").split("\\n")))); - assertVersion(new Version("GraalVM 23.0.0", "23.0.0", ORACLE), MANDREL, + assertVersion(new Version("GraalVM 23.0.0", "23.0.0", GRAALVM), MANDREL, Version.of(Stream.of(("native-image 17.0.7 2023-04-18\n" + "OpenJDK Runtime Environment Mandrel-23.0.0.0-Final (build 17.0.7+7)\n" + "OpenJDK 64-Bit Server VM Mandrel-23.0.0.0-Final (build 17.0.7+7, mixed mode)").split("\\n")))); // should also work when the image is not around and we have to download it - assertVersion(new Version("GraalVM 23.0.0", "23.0.0", ORACLE), MANDREL, + assertVersion(new Version("GraalVM 23.0.0", "23.0.0", GRAALVM), MANDREL, Version.of( Stream.of(("Unable to find image 'quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-17' locally\n" + "jdk-17: Pulling from quarkus/ubi-quarkus-mandrel-builder-image\n" @@ -57,31 +57,31 @@ public void testGraalVMVersionDetected() { + "OpenJDK Runtime Environment Mandrel-23.0.0.0-Final (build 17.0.7+7)\n" + "OpenJDK 64-Bit Server VM Mandrel-23.0.0.0-Final (build 17.0.7+7, mixed mode)") .split("\\n")))); - assertVersion(new Version("GraalVM 23.0", "23.0", ORACLE), ORACLE, + assertVersion(new Version("GraalVM 23.0", "23.0", GRAALVM), GRAALVM, Version.of(Stream.of(("native-image 20 2023-03-21\n" + "GraalVM Runtime Environment GraalVM CE (build 20+34-jvmci-23.0-b10)\n" + "Substrate VM GraalVM CE (build 20+34, serial gc)").split("\\n")))); // Older version parsing - assertVersion(new Version("GraalVM 20.1", "20.1", ORACLE), ORACLE, + assertVersion(new Version("GraalVM 20.1", "20.1", GRAALVM), GRAALVM, Version.of(Stream.of("GraalVM Version 20.1.0 (Java Version 11.0.7)"))); - assertVersion(new Version("GraalVM 20.1.0.1", "20.1.0.1", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 20.1.0.1", "20.1.0.1", GRAALVM), MANDREL, Version .of(Stream.of("GraalVM Version 20.1.0.1.Alpha2 56d4ee1b28 (Mandrel Distribution) (Java Version 11.0.8)"))); - assertVersion(new Version("GraalVM 20.1.0.1", "20.1.0.1", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 20.1.0.1", "20.1.0.1", GRAALVM), MANDREL, Version .of(Stream.of("GraalVM Version 20.1.0.1-Final 56d4ee1b28 (Mandrel Distribution) (Java Version 11.0.8)"))); - assertVersion(new Version("GraalVM 21.0", "21.0", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 21.0", "21.0", GRAALVM), MANDREL, Version .of(Stream.of("GraalVM Version 21.0.0.0-0b3 (Mandrel Distribution) (Java Version 11.0.8)"))); - assertVersion(new Version("GraalVM 20.3.1.2", "20.3.1.2", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 20.3.1.2", "20.3.1.2", GRAALVM), MANDREL, Version .of(Stream.of("GraalVM Version 20.3.1.2-dev (Mandrel Distribution) (Java Version 11.0.8)"))); - assertVersion(new Version("GraalVM 21.1", "21.1", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 21.1", "21.1", GRAALVM), MANDREL, Version .of(Stream.of("native-image 21.1.0.0-Final (Mandrel Distribution) (Java Version 11.0.11+9)"))); - assertVersion(new Version("GraalVM 21.1", "21.1", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 21.1", "21.1", GRAALVM), MANDREL, Version .of(Stream.of("GraalVM 21.1.0.0-Final (Mandrel Distribution) (Java Version 11.0.11+9)"))); - assertVersion(new Version("GraalVM 21.1", "21.1", ORACLE), ORACLE, Version + assertVersion(new Version("GraalVM 21.1", "21.1", GRAALVM), GRAALVM, Version .of(Stream.of("GraalVM 21.1.0 Java 11 CE (Java Version 11.0.11+5-jvmci-21.1-b02)"))); - assertVersion(new Version("GraalVM 21.1", "21.1", ORACLE), ORACLE, Version + assertVersion(new Version("GraalVM 21.1", "21.1", GRAALVM), GRAALVM, Version .of(Stream.of("native-image 21.1.0.0 Java 11 CE (Java Version 11.0.11+5-jvmci-21.1-b02)"))); - assertVersion(new Version("GraalVM 21.2", "21.2", ORACLE), MANDREL, Version + assertVersion(new Version("GraalVM 21.2", "21.2", GRAALVM), MANDREL, Version .of(Stream.of("native-image 21.2.0.0-Final Mandrel Distribution (Java Version 11.0.12+7)"))); } @@ -93,6 +93,39 @@ static void assertVersion(Version graalVmVersion, Distribution distro, Version v } } + @Test + public void testGraalVM21VersionParser() { + Version graalVM21Dev = Version.of(Stream.of(("native-image 21 2023-09-19\n" + + "GraalVM Runtime Environment GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15)\n" + + "Substrate VM GraalVM CE 21+35.1 (build 21+35, serial gc)").split("\\n"))); + assertThat(graalVM21Dev.distribution.name()).isEqualTo("GRAALVM"); + assertThat(graalVM21Dev.getVersionAsString()).isEqualTo("23.1"); + assertThat(graalVM21Dev.javaFeatureVersion).isEqualTo(21); + assertThat(graalVM21Dev.javaUpdateVersion).isEqualTo(0); + } + + @Test + public void testGraalVM21DevVersionParser() { + Version graalVM21Dev = Version.of(Stream.of(("native-image 21 2023-09-19\n" + + "GraalVM Runtime Environment GraalVM CE 21-dev+35.1 (build 21+35-jvmci-23.1-b14)\n" + + "Substrate VM GraalVM CE 21-dev+35.1 (build 21+35, serial gc)").split("\\n"))); + assertThat(graalVM21Dev.distribution.name()).isEqualTo("GRAALVM"); + assertThat(graalVM21Dev.getVersionAsString()).isEqualTo("23.1-dev"); + assertThat(graalVM21Dev.javaFeatureVersion).isEqualTo(21); + assertThat(graalVM21Dev.javaUpdateVersion).isEqualTo(0); + } + + @Test + public void testGraalVM22DevVersionParser() { + Version graalVM22Dev = Version.of(Stream.of(("native-image 22 2024-03-19\n" + + "GraalVM Runtime Environment GraalVM CE 22-dev+16.1 (build 22+16-jvmci-b01)\n" + + "Substrate VM GraalVM CE 22-dev+16.1 (build 22+16, serial gc)").split("\\n"))); + assertThat(graalVM22Dev.distribution.name()).isEqualTo("GRAALVM"); + assertThat(graalVM22Dev.getVersionAsString()).isEqualTo("24.0-dev"); + assertThat(graalVM22Dev.javaFeatureVersion).isEqualTo(22); + assertThat(graalVM22Dev.javaUpdateVersion).isEqualTo(0); + } + @Test public void testGraalVMVersionsOlderThan() { assertOlderThan("GraalVM Version 19.3.6 CE", "GraalVM Version 20.2.0 (Java Version 11.0.9)"); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java index b197c7d91814c0..6a28d740a1713b 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java @@ -1,17 +1,30 @@ package io.quarkus.runtime.configuration; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOCATIONS; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.ConfigSource; import org.jboss.logging.Logger; import io.quarkus.runtime.ImageMode; +import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.common.utils.StringUtil; /** @@ -152,4 +165,79 @@ public static String getNiceErrorMessage() { public static Set getErrorKeys() { return new HashSet<>(errorKeys); } + + private static final DirectoryStream.Filter CONFIG_FILES_FILTER = new DirectoryStream.Filter<>() { + @Override + public boolean accept(final Path entry) { + // Ignore .properties, because we know these are have a default loader in core + // Ignore profile files. The loading rules require the main file to be present, so we only need the type + String filename = entry.getFileName().toString(); + return Files.isRegularFile(entry) && filename.startsWith("application.") && !filename.endsWith(".properties"); + } + }; + + public static Set configFiles(Path configFilesLocation) throws IOException { + if (!Files.exists(configFilesLocation)) { + return Collections.emptySet(); + } + + Set configFiles = new HashSet<>(); + try (DirectoryStream candidates = Files.newDirectoryStream(configFilesLocation, CONFIG_FILES_FILTER)) { + for (Path candidate : candidates) { + configFiles.add(candidate.toString()); + } + } + return configFiles; + } + + public static Set configFilesFromLocations() throws Exception { + SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + + Set configFiles = new HashSet<>(); + configFiles.addAll(configFiles(Paths.get(System.getProperty("user.dir"), "config"))); + Optional> optionalLocations = config.getOptionalValues(SMALLRYE_CONFIG_LOCATIONS, URI.class); + optionalLocations.ifPresent(new Consumer>() { + @Override + public void accept(final List locations) { + for (URI location : locations) { + Path path = location.getScheme() != null && location.getScheme().equals("file") ? Paths.get(location) + : Paths.get(location.getPath()); + if (Files.isDirectory(path)) { + try { + configFiles.addAll(configFiles(path)); + } catch (IOException e) { + // Ignore + } + } + } + } + }); + + return configFiles; + } + + public static void unknownConfigFiles(final Set configFiles) { + SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + Set configNames = new HashSet<>(); + for (ConfigSource configSource : config.getConfigSources()) { + if (configSource.getName() != null && configSource.getName().contains("application")) { + configNames.add(configSource.getName()); + } + } + + for (String configFile : configFiles) { + boolean found = false; + for (String configName : configNames) { + if (configName.contains(configFile)) { + found = true; + break; + } + } + if (!found) { + log.warnf( + "Unrecognized configuration file %s found; Please, check if your are providing the proper extension to load the file", + configFile); + } + } + } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java index 2e42d598f7e955..fd04bf02a88175 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java @@ -97,4 +97,8 @@ public void handleNativeProfileChange(List buildProfiles) { } } } + + public void unknownConfigFiles() throws Exception { + ConfigDiagnostic.unknownConfigFiles(ConfigDiagnostic.configFilesFromLocations()); + } } diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 230bd9f29e5e79..22c2cca510f18d 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1591,6 +1591,19 @@ + + io.quarkus + quarkus-oidc-db-token-state-manager + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc-token-propagation diff --git a/docs/pom.xml b/docs/pom.xml index 5d8fd796d6c594..d28aa46df1760d 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1607,6 +1607,19 @@ + + io.quarkus + quarkus-oidc-db-token-state-manager-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc-token-propagation-deployment diff --git a/docs/src/main/asciidoc/ansible.adoc b/docs/src/main/asciidoc/ansible.adoc index 476cd7c5fdb3ea..696d5d4bed9221 100644 --- a/docs/src/main/asciidoc/ansible.adoc +++ b/docs/src/main/asciidoc/ansible.adoc @@ -1,3 +1,8 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// = Automate Quarkus deployment with Ansible include::_attributes.adoc[] :categories: command-line diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index 08af52f56e101e..9cb0544b829503 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Contexts and Dependency Injection include::_attributes.adoc[] :categories: core +:keywords: arc :summary: Go more in depth into the Quarkus implementation of CDI. :numbered: :sectnums: @@ -92,7 +93,7 @@ You can find the latest plugin version in the https://plugins.gradle.org/plugin/ [source,kotlin,subs=attributes+] ---- plugins { - id("org.kordamp.gradle.jandex") version '{jandex-gradle-plugin-version}' + id("org.kordamp.gradle.jandex") version "{jandex-gradle-plugin-version}" } ---- You can find the latest plugin version in the https://plugins.gradle.org/plugin/org.kordamp.gradle.jandex[Gradle Plugin Portal] diff --git a/docs/src/main/asciidoc/cdi.adoc b/docs/src/main/asciidoc/cdi.adoc index f7fc301f48c72c..dd4d69fb268211 100644 --- a/docs/src/main/asciidoc/cdi.adoc +++ b/docs/src/main/asciidoc/cdi.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Introduction to Contexts and Dependency Injection (CDI) include::_attributes.adoc[] :categories: core +:keywords: qualifier event interceptor observer arc :summary: Quarkus DI solution is based on the [Jakarta Contexts and Dependency Injection 4.0](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html) specification. This guide explains the basics of CDI. :numbered: :sectnums: @@ -28,7 +29,7 @@ It creates and destroys the instances of beans, associates the instances with a An application developer can focus on the business logic rather than finding out "where and how" to obtain a fully initialized component with all of its dependencies. -NOTE: You've probably heard of the _inversion of control_ (IoC) programming principle. Dependency injection is one of the implementation techniques of IoC. +NOTE: You've probably heard of the _inversion of control_ (IoC) programming principle. Dependency injection is one of the implementation techniques of IoC. == What does a bean look like? @@ -47,9 +48,9 @@ public class Translator { @Inject Dictionary dictionary; <2> - + @Counted <3> - String translate(String sentence) { + String translate(String sentence) { // ... } } @@ -86,8 +87,8 @@ public class Translator { @Inject Instance dictionaries; <1> - - String translate(String sentence) { + + String translate(String sentence) { for (Dictionary dict : dictionaries) { <2> // ... } @@ -122,11 +123,11 @@ public class Translator { } } ---- -<1> This is a constructor injection. -In fact, this code would not work in regular CDI implementations where a bean with a normal scope must always declare a no-args constructor and the bean constructor must be annotated with `@Inject`. +<1> This is a constructor injection. +In fact, this code would not work in regular CDI implementations where a bean with a normal scope must always declare a no-args constructor and the bean constructor must be annotated with `@Inject`. However, in Quarkus we detect the absence of no-args constructor and "add" it directly in the bytecode. It's also not necessary to add `@Inject` if there is only one constructor present. -<2> An initializer method must be annotated with `@Inject`. +<2> An initializer method must be annotated with `@Inject`. <3> An initializer may accept multiple parameters - each one is an injection point. == You talked about some qualifiers? @@ -155,7 +156,7 @@ The qualifiers of a bean are declared by annotating the bean class or producer m @ApplicationScoped public class SuperiorTranslator extends Translator { - String translate(String sentence) { + String translate(String sentence) { // ... } } @@ -180,11 +181,11 @@ You can use all the built-in scopes mentioned by the specification except for `j [options="header",cols="1,1"] |=== -|Annotation |Description +|Annotation |Description //---------------------- -|`@jakarta.enterprise.context.ApplicationScoped` | A single bean instance is used for the application and shared among all injection points. The instance is created lazily, i.e. once a method is invoked upon the <>. +|`@jakarta.enterprise.context.ApplicationScoped` | A single bean instance is used for the application and shared among all injection points. The instance is created lazily, i.e. once a method is invoked upon the <>. |`@jakarta.inject.Singleton` | Just like `@ApplicationScoped` except that no client proxy is used. The instance is created when an injection point that resolves to a @Singleton bean is being injected. -|`@jakarta.enterprise.context.RequestScoped` | The bean instance is associated with the current _request_ (usually an HTTP request). +|`@jakarta.enterprise.context.RequestScoped` | The bean instance is associated with the current _request_ (usually an HTTP request). |`@jakarta.enterprise.context.Dependent` | This is a pseudo-scope. The instances are not shared and every injection point spawns a new instance of the dependent bean. The lifecycle of dependent bean is bound to the bean injecting it - it will be created and destroyed along with the bean injecting it. |`@jakarta.enterprise.context.SessionScoped` | This scope is backed by a `jakarta.servlet.http.HttpSession` object. It's only available if the `quarkus-undertow` extension is used. |=== @@ -217,7 +218,7 @@ Indeed, the https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html# A client proxy is basically an object that delegates all method invocations to a target bean instance. It's a container construct that implements `io.quarkus.arc.ClientProxy` and extends the bean class. -IMPORTANT: Client proxies only delegate method invocations. So never read or write a field of a normal scoped bean, otherwise you will work with non-contextual or stale data. +IMPORTANT: Client proxies only delegate method invocations. So never read or write a field of a normal scoped bean, otherwise you will work with non-contextual or stale data. .Generated Client Proxy Example [source,java] @@ -225,7 +226,7 @@ IMPORTANT: Client proxies only delegate method invocations. So never read or wri @ApplicationScoped class Translator { - String translate(String sentence) { + String translate(String sentence) { // ... } } @@ -233,7 +234,7 @@ class Translator { // The client proxy class is generated and looks like... class Translator_ClientProxy extends Translator { <1> - String translate(String sentence) { + String translate(String sentence) { // Find the correct translator instance... Translator translator = getTranslatorInstanceFromTheApplicationContext(); // And delegate the method invocation... @@ -247,10 +248,10 @@ Client proxies allow for: * Lazy instantiation - the instance is created once a method is invoked upon the proxy. * Ability to inject a bean with "narrower" scope to a bean with "wider" scope; i.e. you can inject a `@RequestScoped` bean into an `@ApplicationScoped` bean. -* Circular dependencies in the dependency graph. Having circular dependencies is often an indication that a redesign should be considered, but sometimes it's inevitable. +* Circular dependencies in the dependency graph. Having circular dependencies is often an indication that a redesign should be considered, but sometimes it's inevitable. * In rare cases it's practical to destroy the beans manually. A direct injected reference would lead to a stale bean instance. - - + + == OK. You said that there are several kinds of beans? Yes. In general, we distinguish: @@ -273,7 +274,7 @@ public class Producers { @Produces <1> double pi = Math.PI; <2> - + @Produces <3> List names() { List names = new ArrayList<>(); @@ -289,26 +290,26 @@ public class Consumer { @Inject double pi; - + @Inject List names; - - // ... -} + + // ... +} ---- <1> The container analyses the field annotations to build a bean metadata. -The _type_ is used to build the set of bean types. +The _type_ is used to build the set of bean types. In this case, it will be `double` and `java.lang.Object`. No scope annotation is declared and so it's defaulted to `@Dependent`. <2> The container will read this field when creating the bean instance. <3> The container analyses the method annotations to build a bean metadata. -The _return type_ is used to build the set of bean types. +The _return type_ is used to build the set of bean types. In this case, it will be `List`, `Collection`, `Iterable` and `java.lang.Object`. No scope annotation is declared and so it's defaulted to `@Dependent`. <4> The container will call this method when creating the bean instance. -There's more about producers. -You can declare qualifiers, inject dependencies into the producer methods parameters, etc. +There's more about producers. +You can declare qualifiers, inject dependencies into the producer methods parameters, etc. You can read more about producers for example in the https://docs.jboss.org/weld/reference/latest/en-US/html/producermethods.html[Weld docs, window="_blank"]. == OK, injection looks cool. What other services are provided? @@ -330,7 +331,7 @@ public class Translator { void init() { // ... } - + @PreDestroy <2> void destroy() { // ... @@ -345,7 +346,7 @@ TIP: It's a good practice to keep the logic in the callbacks "without side effec [[interceptors]] === Interceptors -Interceptors are used to separate cross-cutting concerns from business logic. +Interceptors are used to separate cross-cutting concerns from business logic. There is a separate specification - Java Interceptors - that defines the basic programming model and semantics. .Simple Interceptor Binding Example @@ -392,11 +393,11 @@ public class LoggingInterceptor { // ...log after return ret; } - + } ---- <1> The interceptor binding annotation is used to bind our interceptor to a bean. Simply annotate a bean class with `@Logged`, as in the following example. -<2> `Priority` enables the interceptor and affects the interceptor ordering. Interceptors with smaller priority values are called first. +<2> `Priority` enables the interceptor and affects the interceptor ordering. Interceptors with smaller priority values are called first. <3> Marks an interceptor component. <4> An interceptor may inject dependencies. <5> `AroundInvoke` denotes a method that interposes on business methods. @@ -448,7 +449,7 @@ public class LargeTxAccount implements Account { <3> @Any @Delegate Account delegate; <4> - + @Inject LogService logService; <5> @@ -458,10 +459,10 @@ public class LargeTxAccount implements Account { <3> logService.logWithdrawal(delegate, amount); } } - + } ---- -<1> `@Priority` enables the decorator. Decorators with smaller priority values are called first. +<1> `@Priority` enables the decorator. Decorators with smaller priority values are called first. <2> `@Decorator` marks a decorator component. <3> The set of decorated types includes all bean types which are Java interfaces, except for `java.io.Serializable`. <4> Each decorator must declare exactly one _delegate injection point_. The decorator applies to beans that are assignable to this delegate injection point. @@ -471,7 +472,7 @@ public class LargeTxAccount implements Account { <3> NOTE: Instances of decorators are dependent objects of the bean instance they intercept, i.e. a new decorator instance is created for each intercepted bean. === Events and Observers - + Beans may also produce and consume events to interact in a completely decoupled fashion. Any Java object can serve as an event payload. The optional qualifiers act as topic selectors. diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index dcbdb6106e1fd5..d48bad282bb1fb 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Quarkus Extension Metadata - include::_attributes.adoc[] Quarkus extensions are distributed as Maven JAR artifacts that application and other libraries may depend on. When a Quarkus application project is built, tested or edited using the Quarkus dev tools, Quarkus extension JAR artifacts will be identified on the application classpath by the presence of the Quarkus extension metadata files in them. @@ -127,11 +126,11 @@ The following properties may appear in this file: | `parent-first-artifacts` | Optional -| Comma-separated list of artifact keys (`groupId:artifactId:classifier:type`) that are to be loaded in a parent first manner. This can be used to work around issues where a given class needs to be loaded by the system ClassLoader. +| Comma-separated list of artifact keys (`groupId:artifactId:classifier:type`) that are to be loaded in a parent first manner. This can be used to work around issues where a given class needs to be loaded by the system ClassLoader. | `runner-parent-first-artifacts` | Optional -| Comma-separated list of artifact keys that are to be loaded in a parent first manner in addition to those configured with `parent-first-artifacts` when an application is launched from a production binary package. This can be used to work around issues where a given class needs to be loaded by the system ClassLoader. +| Comma-separated list of artifact keys that are to be loaded in a parent first manner in addition to those configured with `parent-first-artifacts` when an application is launched from a production binary package. This can be used to work around issues where a given class needs to be loaded by the system ClassLoader. | `excluded-artifacts` | Optional @@ -143,7 +142,7 @@ The following properties may appear in this file: | `removed-resources.*` | Optional -| Resources that should be removed/hidden from dependencies. This allows for classes and other resources to be removed from dependencies, so they are not accessible to the application. This is a map of artifact key to a comma-separated list of resources to be removed. When running in dev and test mode these resources are hidden from the ClassLoader, when running in production mode these files are removed from the jars that contain them. Note that if you want to remove a class you need to specify the class file name. e.g. to remove com.acme.Foo you would specify com/acme/Foo.class. +| Resources that should be removed/hidden from dependencies. This allows for classes and other resources to be removed from dependencies, so they are not accessible to the application. This is a map of artifact key to a comma-separated list of resources to be removed. When running in dev and test mode these resources are hidden from the ClassLoader, when running in production mode these files are removed from the jars that contain them. Note that if you want to remove a class you need to specify the class file name. e.g. to remove com.acme.Foo you would specify com/acme/Foo.class. | `provides-capabilities` | Optional diff --git a/docs/src/main/asciidoc/grpc-virtual-threads.adoc b/docs/src/main/asciidoc/grpc-virtual-threads.adoc index eb027e7b7d375f..9f4445b003c38a 100644 --- a/docs/src/main/asciidoc/grpc-virtual-threads.adoc +++ b/docs/src/main/asciidoc/grpc-virtual-threads.adoc @@ -1,5 +1,9 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// = Quarkus Virtual Thread support for gRPC services - include::_attributes.adoc[] :runonvthread: https://javadoc.io/doc/io.smallrye.common/smallrye-common-annotation/latest/io/smallrye/common/annotation/RunOnVirtualThread.html :blocking_annotation: https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/latest/io/smallrye/reactive/messaging/annotations/Blocking.html diff --git a/docs/src/main/asciidoc/lifecycle.adoc b/docs/src/main/asciidoc/lifecycle.adoc index 71416ecb0c09a5..400cdc552b90d2 100644 --- a/docs/src/main/asciidoc/lifecycle.adoc +++ b/docs/src/main/asciidoc/lifecycle.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Application Initialization and Termination include::_attributes.adoc[] :categories: core +:keywords: lifecycle event :summary: You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to be notified when an application stops or starts. You often need to execute custom actions when the application starts and clean up everything when the application stops. @@ -175,7 +176,7 @@ NOTE: The methods can access injected beans. Check the link:{quickstarts-blob-ur === What is the difference from `@Initialized(ApplicationScoped.class)` and `@Destroyed(ApplicationScoped.class)` -In the JVM mode, there is no real difference, except that `StartupEvent` is always fired *after* `@Initialized(ApplicationScoped.class)` and `ShutdownEvent` is fired *before* `@Destroyed(ApplicationScoped.class)`. +In the JVM mode, there is no real difference, except that `StartupEvent` is always fired *after* `@Initialized(ApplicationScoped.class)` and `ShutdownEvent` is fired *before* `@Destroyed(ApplicationScoped.class)`. For a native executable build, however, `@Initialized(ApplicationScoped.class)` is fired as *part of the native build process*, whereas `StartupEvent` is fired when the native image is executed. See xref:writing-extensions.adoc#bootstrap-three-phases[Three Phases of Bootstrap and Quarkus Philosophy] for more details. @@ -193,10 +194,10 @@ package org.acme.lifecycle; import io.quarkus.runtime.Startup; import jakarta.enterprise.context.ApplicationScoped; -@Startup // <1> +@Startup // <1> @ApplicationScoped public class EagerAppBean { - + private final String name; EagerAppBean(NameGenerator generator) { // <2> @@ -222,10 +223,10 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class EagerAppBean { - + @Startup void init() { <1> - doSomeCoolInit(); + doSomeCoolInit(); } } ---- @@ -234,7 +235,7 @@ public class EagerAppBean { [[shutdown_annotation]] === Using `@Shutdown` to execute a business method of a CDI bean during application shutdown -The `@io.quarkus.runtime.Shutdown` annotation is used to mark a business method of a CDI bean that should be executed during application shutdown. +The `@io.quarkus.runtime.Shutdown` annotation is used to mark a business method of a CDI bean that should be executed during application shutdown. The annotated method must be non-private and non-static and declare no arguments. The behavior is similar to a declaration of a `ShutdownEvent` observer. The following examples are functionally equivalent. diff --git a/docs/src/main/asciidoc/liquibase-mongodb.adoc b/docs/src/main/asciidoc/liquibase-mongodb.adoc index 5cacde3b5283e5..a9e37fcab7f21a 100644 --- a/docs/src/main/asciidoc/liquibase-mongodb.adoc +++ b/docs/src/main/asciidoc/liquibase-mongodb.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Using Liquibase MongoDB - include::_attributes.adoc[] :change-log: src/main/resources/db/changeLog.xml :config-file: application.properties diff --git a/docs/src/main/asciidoc/messaging-virtual-threads.adoc b/docs/src/main/asciidoc/messaging-virtual-threads.adoc index 8b633f0d3aec8e..ccf8a9f0b6ad60 100644 --- a/docs/src/main/asciidoc/messaging-virtual-threads.adoc +++ b/docs/src/main/asciidoc/messaging-virtual-threads.adoc @@ -1,5 +1,9 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// = Quarkus Virtual Thread support with Reactive Messaging - include::_attributes.adoc[] :runonvthread: https://javadoc.io/doc/io.smallrye.common/smallrye-common-annotation/latest/io/smallrye/common/annotation/RunOnVirtualThread.html :rm_blocking_annotation: https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/latest/io/smallrye/reactive/messaging/annotations/Blocking.html diff --git a/docs/src/main/asciidoc/mutiny-primer.adoc b/docs/src/main/asciidoc/mutiny-primer.adoc index 46385eb8d937c4..78753c348d8b6f 100644 --- a/docs/src/main/asciidoc/mutiny-primer.adoc +++ b/docs/src/main/asciidoc/mutiny-primer.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Mutiny - Async for bare mortal - include::_attributes.adoc[] https://smallrye.io/smallrye-mutiny[Mutiny] is an intuitive, reactive programming library. diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index ae8f3e248007cb..669f54e51f43d8 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -496,8 +496,12 @@ The instrumentation documented in this section has been tested with Quarkus and Annotating a method in any CDI aware bean with the `io.opentelemetry.instrumentation.annotations.WithSpan` annotation will create a new Span and establish any required relationships with the current Trace context. +Annotating a method in any CDI aware bean with the `io.opentelemetry.instrumentation.annotations.AddingSpanAttributes` will not create a new span but will add annotated method parameters to attributes in the current span. + +If a method is annotated by mistake with `@AddingSpanAttributes` and `@WithSpan` annotations, the `@WithSpan` annotation will take precedence. + Method parameters can be annotated with the `io.opentelemetry.instrumentation.annotations.SpanAttribute` annotation to -indicate which method parameters should be part of the Trace. +indicate which method parameters should be part of the span. The parameter name can be customized as well. Example: [source,java] @@ -523,6 +527,11 @@ class SpanBean { void spanArgs(@SpanAttribute(value = "arg") String arg) { } + + @AddingSpanAttributes + void addArgumentToExistingSpan(@SpanAttribute(value = "arg") String arg) { + + } } ---- diff --git a/docs/src/main/asciidoc/pulsar-dev-services.adoc b/docs/src/main/asciidoc/pulsar-dev-services.adoc index e8e3d8ac1c6eb9..3a40ab216db97b 100644 --- a/docs/src/main/asciidoc/pulsar-dev-services.adoc +++ b/docs/src/main/asciidoc/pulsar-dev-services.adoc @@ -62,6 +62,6 @@ The following example enables transaction support: [source, properties] ---- -quarkus.pulsar.devservices.broker-config.transactionCoordinatorEnabled=true -quarkus.pulsar.devservices.broker-config.systemTopicEnabled=true +quarkus.pulsar.devservices.broker-config.transaction-coordinator-enabled=true +quarkus.pulsar.devservices.broker-config.system-topic-enabled=true ---- diff --git a/docs/src/main/asciidoc/quarkus-maven-plugin.adoc b/docs/src/main/asciidoc/quarkus-maven-plugin.adoc index aadebc85ea9e60..ae1c766b2691fa 100644 --- a/docs/src/main/asciidoc/quarkus-maven-plugin.adoc +++ b/docs/src/main/asciidoc/quarkus-maven-plugin.adoc @@ -4,12 +4,11 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Quarkus Maven Plugin +include::_attributes.adoc[] The Quarkus Maven Plugin builds the Quarkus applications, and provides helpers to launch dev mode or build native executables. For more information about how to use the Quarkus Maven Plugin, please refer to the xref:maven-tooling.adoc[Maven Tooling guide]. -include::_attributes.adoc[] - == Discover Maven goals Like most Maven plugins, the Quarkus Maven Plugin has a `help` goal that prints the description of the plugin, listing all available goals as well as their description. diff --git a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc index c1c2ee282971b6..c891734ead8300 100644 --- a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc +++ b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Quarkus Base Runtime Image - include::_attributes.adoc[] To ease the containerization of native executables, Quarkus provides a base image providing the requirements to run these executables. diff --git a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc index bbcdef597b99aa..8454327bd5b152 100644 --- a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc +++ b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Dev Services for RabbitMQ - include::_attributes.adoc[] Dev Services for RabbitMQ automatically starts a RabbitMQ broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/rabbitmq-reference.adoc b/docs/src/main/asciidoc/rabbitmq-reference.adoc index 105a674eb49401..84271eff319e75 100644 --- a/docs/src/main/asciidoc/rabbitmq-reference.adoc +++ b/docs/src/main/asciidoc/rabbitmq-reference.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Reactive Messaging RabbitMQ Connector Reference Documentation - include::_attributes.adoc[] This guide is the companion from the xref:rabbitmq.adoc[Getting Started with RabbitMQ]. diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index a49ae779c37c10..fa25027b7ce458 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Redis Extension Reference Guide - :extension-status: preview include::_attributes.adoc[] :numbered: diff --git a/docs/src/main/asciidoc/resteasy.adoc b/docs/src/main/asciidoc/resteasy.adoc index 39ac40d3b20a26..7d608174e0cf0f 100644 --- a/docs/src/main/asciidoc/resteasy.adoc +++ b/docs/src/main/asciidoc/resteasy.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = RESTEasy Classic - include::_attributes.adoc[] [WARNING] @@ -210,7 +209,7 @@ public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer { Users can even provide their own `ObjectMapper` bean if they so choose. If this is done, it is very important to manually inject and apply all `io.quarkus.jackson.ObjectMapperCustomizer` beans in the CDI producer that produces `ObjectMapper`. -Failure to do so will prevent Jackson specific customizations provided by various extensions from being applied. +Failure to do so will prevent Jackson specific customizations provided by various extensions from being applied. [source,java] ---- @@ -293,7 +292,7 @@ public class CustomJsonbConfig { [[links]] === JSON Hypertext Application Language (HAL) support -The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a simple format to represent web links. +The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a simple format to represent web links. To enable the HAL support, add the `quarkus-hal` extension to your project. Also, as HAL needs JSON support, you need to add either the `quarkus-resteasy-jsonb` or the `quarkus-resteasy-jackson` extension. @@ -330,7 +329,7 @@ public class RecordsResource { } ---- -Now, the endpoints `/records` and `/records/first` will accept the media type both `json` and `hal+json` to print the records in Hal format. +Now, the endpoints `/records` and `/records/first` will accept the media type both `json` and `hal+json` to print the records in Hal format. For example, if we invoke the `/records` endpoint using curl to return a list of records, the HAL format will look like as follows: diff --git a/docs/src/main/asciidoc/scripting.adoc b/docs/src/main/asciidoc/scripting.adoc index c5a4a11a99a5b7..a09fe783cf3830 100644 --- a/docs/src/main/asciidoc/scripting.adoc +++ b/docs/src/main/asciidoc/scripting.adoc @@ -42,7 +42,6 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import org.jboss.logging.Logger; @Path("/hello") @ApplicationScoped @@ -136,9 +135,9 @@ $ jbang quarkusapp.java [jbang] Dependencies resolved [jbang] Building jar... [jbang] Post build with io.quarkus.launcher.JBangIntegration -Aug 30, 2020 5:40:55 AM org.jboss.threads.Version -INFO: JBoss Threads version 3.1.1.Final -Aug 30, 2020 5:40:56 AM io.quarkus.deployment.QuarkusAugmentor run +Mar 22, 2023 9:47:51 A.M. org.jboss.threads.Version +INFO: JBoss Threads version 3.5.0.Final +Mar 22, 2023 9:47:51 A.M. io.quarkus.deployment.QuarkusAugmentor run INFO: Quarkus augmentation completed in 722ms Hello World ---- @@ -148,13 +147,9 @@ For now the application does nothing new. [TIP] .How do I edit this file and get content assist? ==== -As there is nothing but a `.java` file, most IDE's don't handle content assist well. -To work around that you can run `jbang edit quarkusapp.java`, this will print out a directory that will have a temporary project setup you can use in your IDE. +To edit the JBang script in an IDE/editor with content assist you can run `jbang edit quarkusapp.java` or `jbang edit quarkusapp.java`. -On Linux/macOS you can run ` `jbang edit quarkusapp.java``. - -If you add dependencies while editing, you can get JBang to automatically refresh -the IDE project using `jbang edit --live= quarkusapp.java`. +For more information please refer to the https://www.jbang.dev/documentation/guide/latest/editing.html[the JBang documentation]. ==== @@ -204,17 +199,17 @@ $ jbang quarkusapp.java [jbang] Building jar... [jbang] Post build with io.quarkus.launcher.JBangIntegration -Aug 30, 2020 5:49:01 AM org.jboss.threads.Version -INFO: JBoss Threads version 3.1.1.Final -Aug 30, 2020 5:49:02 AM io.quarkus.deployment.QuarkusAugmentor run -INFO: Quarkus augmentation completed in 681ms +Mar 22, 2023 9:48:39 A.M. org.jboss.threads.Version +INFO: JBoss Threads version 3.5.0.Final +Mar 22, 2023 9:48:39 A.M. io.quarkus.deployment.QuarkusAugmentor run +INFO: Quarkus augmentation completed in 521ms __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ -2020-08-30 05:49:03,255 INFO [io.quarkus] (main) Quarkus {quarkus-version} on JVM started in 0.638s. Listening on: http://0.0.0.0:8080 -2020-08-30 05:49:03,272 INFO [io.quarkus] (main) Profile prod activated. -2020-08-30 05:49:03,272 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive] +2023-03-22 09:48:39,891 INFO [io.quarkus] (main) quarkus 999-SNAPSHOT on JVM (powered by Quarkus {quarkus-version}) started in 0.283s. Listening on: http://0.0.0.0:8080 +2023-03-22 09:48:39,904 INFO [io.quarkus] (main) Profile prod activated. +2023-03-22 09:48:39,904 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, smallrye-context-propagation, vertx] ---- Once started, you can request the provided endpoint: @@ -281,7 +276,8 @@ Edit the `quarksapp` class to inject the `GreetingService` and create a new endp [source,java,subs=attributes+] ---- //usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.quarkus:quarkus-resteasy-reactive:{quarkus-version} +//DEPS io.quarkus.platform:quarkus-bom:{quarkus-version}@pom +//DEPS io.quarkus:quarkus-resteasy-reactive import io.quarkus.runtime.Quarkus; import jakarta.enterprise.context.ApplicationScoped; @@ -297,7 +293,7 @@ public class quarkusapp { @GET public String sayHello() { - return "hello from Quarkus with jbang.dev"; + return "hello"; } public static void main(String[] args) { @@ -393,34 +389,40 @@ Now during build the `quarkus.swagger-ui.always-include` will be generated into == Running as a native application -If you have the `native-image` binary installed and `GRAALVM_HOME` set, you can get the native executable built and run using `jbang --native quarkusapp.java`: +If you have the `native-image` binary installed and `GRAALVM_HOME` set, or a container runtime (e.g., podman or docker) installed on Linux, you can get the native executable built and run using `jbang --native quarkusapp.java`: [source,shell,subs=attributes+] ---- $ jbang --native quarkusapp.java - [jbang] Building jar... [jbang] Post build with io.quarkus.launcher.JBangIntegration -Aug 30, 2020 6:21:15 AM org.jboss.threads.Version -INFO: JBoss Threads version 3.1.1.Final -Aug 30, 2020 6:21:16 AM io.quarkus.deployment.pkg.steps.JarResultBuildStep buildNativeImageThinJar -INFO: Building native image source jar: /var/folders/yb/sytszfld4sg8vwr1h0w20jlw0000gn/T/quarkus-jbang3291688251685023074/quarkus-application-native-image-source-jar/quarkus-application-runner.jar -Aug 30, 2020 6:21:16 AM io.quarkus.deployment.pkg.steps.NativeImageBuildStep build -INFO: Building native image from /var/folders/yb/sytszfld4sg8vwr1h0w20jlw0000gn/T/quarkus-jbang3291688251685023074/quarkus-application-native-image-source-jar/quarkus-application-runner.jar -Aug 30, 2020 6:21:16 AM io.quarkus.deployment.pkg.steps.NativeImageBuildStep checkGraalVMVersion -INFO: Running Quarkus native-image plugin on GraalVM Version 20.1.0 (Java Version 11.0.7) -Aug 30, 2020 6:21:16 AM io.quarkus.deployment.pkg.steps.NativeImageBuildStep build -INFO: /Users/max/.sdkman/candidates/java/20.1.0.r11-grl/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=1 -J-Duser.language=en -J-Dfile.encoding=UTF-8 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+JNI -jar quarkus-application-runner.jar -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:EnableURLProtocols=http --no-server -H:-UseServiceLoaderFeature -H:+StackTrace quarkus-application-runner - -Aug 30, 2020 6:22:31 AM io.quarkus.deployment.QuarkusAugmentor run -INFO: Quarkus augmentation completed in 76010ms +Mar 22, 2023 9:58:47 A.M. org.jboss.threads.Version +INFO: JBoss Threads version 3.5.0.Final +Mar 22, 2023 9:58:47 A.M. io.quarkus.deployment.pkg.steps.JarResultBuildStep buildNativeImageThinJar +INFO: Building native image source jar: /tmp/quarkus-jbang8082065952748314720/quarkus-application-native-image-source-jar/quarkus-application-runner.jar +Mar 22, 2023 9:58:47 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildStep build +INFO: Building native image from /tmp/quarkus-jbang8082065952748314720/quarkus-application-native-image-source-jar/quarkus-application-runner.jar +Mar 22, 2023 9:58:47 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildStep getNativeImageBuildRunner +WARN: Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image` Attempting to fall back to container build. +Mar 22, 2023 9:58:47 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner +INFO: Using docker to run the native image builder +Mar 22, 2023 9:58:47 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner setup +INFO: Checking image status quay.io/quarkus/ubi-quarkus-mandrel-builder-image:22.3-java17 +Mar 22, 2023 9:58:51 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildStep checkGraalVMVersion +INFO: Running Quarkus native-image plugin on native-image 22.3.1.0-Final Mandrel Distribution (Java Version 17.0.6+10) +Mar 22, 2023 9:58:51 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildRunner build +INFO: docker run --env LANG=C --rm --user 1000:1000 -v /tmp/quarkus-jbang8082065952748314720/quarkus-application-native-image-source-jar:/project:z --name build-native-XaZUc quay.io/quarkus/ubi-quarkus-mandrel-builder-image:22.3-java17 -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dlogging.initial-configurator.min-level=500 -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.noUnsafe=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=3 -J-Duser.language=en -J-Duser.country=IE -J-Dfile.encoding=UTF-8 --features=io.quarkus.runner.Feature,io.quarkus.runtime.graal.DisableLoggingFeature -J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED -J--add-opens=java.base/java.text=ALL-UNNAMED -J--add-opens=java.base/java.io=ALL-UNNAMED -J--add-opens=java.base/java.lang.invoke=ALL-UNNAMED -J--add-opens=java.base/java.util=ALL-UNNAMED -H:+CollectImageBuildStatistics -H:ImageBuildStatisticsFile=quarkus-application-runner-timing-stats.json -H:BuildOutputJSONFile=quarkus-application-runner-build-output-stats.json -H:+AllowFoldMethods -J-Djava.awt.headless=true --no-fallback --link-at-build-time -H:+ReportExceptionStackTraces -H:-AddAllCharsets --enable-url-protocols=http -H:NativeLinkerOption=-no-pie -H:-UseServiceLoaderFeature -H:+StackTrace -J--add-exports=org.graalvm.sdk/org.graalvm.nativeimage.impl=ALL-UNNAMED --exclude-config io\.netty\.netty-codec /META-INF/native-image/io\.netty/netty-codec/generated/handlers/reflect-config\.json --exclude-config io\.netty\.netty-handler /META-INF/native-image/io\.netty/netty-handler/generated/handlers/reflect-config\.json quarkus-application-runner -jar quarkus-application-runner.jar +Mar 22, 2023 9:37:56 A.M. io.quarkus.deployment.pkg.steps.NativeImageBuildRunner runCommand +INFO: docker run --env LANG=C --rm --user 1000:1000 -v /tmp/quarkus-jbang9315448339582904220/quarkus-application-native-image-source-jar:/project:z --entrypoint /bin/bash quay.io/quarkus/ubi-quarkus-mandrel-builder-image:22.3-java17 -c objcopy --strip-debug quarkus-application-runner +Mar 22, 2023 9:37:57 A.M. io.quarkus.deployment.QuarkusAugmentor run +INFO: Quarkus augmentation completed in 31729ms __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ -2020-08-30 06:22:32,012 INFO [io.quarkus] (main) Quarkus {quarkus-version} native started in 0.017s. Listening on: http://0.0.0.0:8080 -2020-08-30 06:22:32,013 INFO [io.quarkus] (main) Profile prod activated. -2020-08-30 06:22:32,013 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive] +2023-03-22 09:37:57,471 INFO [io.quarkus] (main) quarkus 999-SNAPSHOT native (powered by {quarkus-version}) started in 0.009s. Listening on: http://0.0.0.0:8080 +2023-03-22 09:37:57,472 INFO [io.quarkus] (main) Profile prod activated. +2023-03-22 09:37:57,472 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, smallrye-context-propagation, vertx] ---- This native build will take some time on first run but any subsequent runs (without changing `quarkusapp.java`) will be close to instant thanks to JBang cache: @@ -432,9 +434,9 @@ __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ -2020-08-30 06:23:36,846 INFO [io.quarkus] (main) Quarkus {quarkus-version} native started in 0.015s. Listening on: http://0.0.0.0:8080 -2020-08-30 06:23:36,846 INFO [io.quarkus] (main) Profile prod activated. -2020-08-30 06:23:36,846 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive] +2023-03-22 09:38:45,450 INFO [io.quarkus] (main) quarkus 999-SNAPSHOT native (powered by {quarkus-version}) started in 0.009s. Listening on: http://0.0.0.0:8080 +2023-03-22 09:38:45,450 INFO [io.quarkus] (main) Profile prod activated. +2023-03-22 09:38:45,450 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, smallrye-context-propagation, vertx] ---- === Conclusion diff --git a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc index 0ccbb5e4268672..70b42c4c88ef4e 100644 --- a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc +++ b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-authentication-mechanisms"] = Authentication mechanisms in Quarkus include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc index 2f620a217c8712..f298a74372419c 100644 --- a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc +++ b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-basic-authentication-howto"] = Enable Basic authentication include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-basic-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-basic-authentication-tutorial.adoc index 1b62aecee6d504..b89c9fdcb33445 100644 --- a/docs/src/main/asciidoc/security-basic-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-basic-authentication-tutorial.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-basic-authentication-tutorial"] = Secure a Quarkus application with Basic authentication and Jakarta Persistence include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-basic-authentication.adoc b/docs/src/main/asciidoc/security-basic-authentication.adoc index 4349139f8179d1..ea702ebe9520be 100644 --- a/docs/src/main/asciidoc/security-basic-authentication.adoc +++ b/docs/src/main/asciidoc/security-basic-authentication.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-basic-authentication"] = Basic authentication include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-csrf-prevention.adoc b/docs/src/main/asciidoc/security-csrf-prevention.adoc index 76e22280dcf906..a417f5f08626d5 100644 --- a/docs/src/main/asciidoc/security-csrf-prevention.adoc +++ b/docs/src/main/asciidoc/security-csrf-prevention.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Cross-Site Request Forgery Prevention - include::_attributes.adoc[] https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)] is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated. @@ -58,14 +57,14 @@ Next, let's add a `csrfToken.html` Qute template producing an HTML form in the ` -User Name Input +User Name Input

User Name Input

<1> - +

Your Name:

diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index 17b513c0e8519a..246358d7731af7 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Security Tips and Tricks - include::_attributes.adoc[] == Quarkus Security Dependency @@ -621,6 +620,6 @@ xref:context-propagation.adoc[Context Propagation Guide]. == References * xref:security-overview.adoc[Quarkus Security overview] -* xref:security-architecture.adoc[Quarkus Security architecture] +* xref:security-architecture.adoc[Quarkus Security architecture] * xref:security-authentication-mechanisms.adoc#other-supported-authentication-mechanisms[Authentication mechanisms in Quarkus] * xref:security-identity-providers.adoc[Identity providers] \ No newline at end of file diff --git a/docs/src/main/asciidoc/security-jpa.adoc b/docs/src/main/asciidoc/security-jpa.adoc index eb5faa68d46988..8a5eaf4d13f1fb 100644 --- a/docs/src/main/asciidoc/security-jpa.adoc +++ b/docs/src/main/asciidoc/security-jpa.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-jpa"] = Quarkus Security with Jakarta Persistence include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-jwt-build.adoc b/docs/src/main/asciidoc/security-jwt-build.adoc index 76365e2c562d5f..15a26f4bb69c42 100644 --- a/docs/src/main/asciidoc/security-jwt-build.adoc +++ b/docs/src/main/asciidoc/security-jwt-build.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Build, Sign and Encrypt JSON Web Tokens - include::_attributes.adoc[] According to link:https://datatracker.ietf.org/doc/html/rfc7519[RFC7519], JSON Web Token (JWT) is a compact, URL-safe means of representing claims which are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code(MAC) and/or encrypted. diff --git a/docs/src/main/asciidoc/security-keycloak-admin-client.adoc b/docs/src/main/asciidoc/security-keycloak-admin-client.adoc index c27caf445b8021..7a82118a0c362f 100644 --- a/docs/src/main/asciidoc/security-keycloak-admin-client.adoc +++ b/docs/src/main/asciidoc/security-keycloak-admin-client.adoc @@ -4,8 +4,9 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Using Keycloak Admin Client - include::_attributes.adoc[] +:categories: security +:keywords: sso oidc security keycloak The Quarkus Keycloak Admin Client and its reactive twin support Keycloak Admin Client which can be used to configure a running Keycloak server. diff --git a/docs/src/main/asciidoc/security-keycloak-authorization.adoc b/docs/src/main/asciidoc/security-keycloak-authorization.adoc index cc64943975af04..ee3c2ec03fae32 100644 --- a/docs/src/main/asciidoc/security-keycloak-authorization.adoc +++ b/docs/src/main/asciidoc/security-keycloak-authorization.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using OpenID Connect (OIDC) and Keycloak to Centralize Authorization include::_attributes.adoc[] :categories: security +:keywords: sso oidc security keycloak :summary: This guide demonstrates how your Quarkus application can authorize access to protected resources using Keycloak Authorization Services. This guide demonstrates how your Quarkus application can authorize a bearer token access to protected resources using https://www.keycloak.org/docs/latest/authorization_services/index.html[Keycloak Authorization Services]. diff --git a/docs/src/main/asciidoc/security-oauth2.adoc b/docs/src/main/asciidoc/security-oauth2.adoc index 91dbb82a9cc5ae..97db6f2de8b953 100644 --- a/docs/src/main/asciidoc/security-oauth2.adoc +++ b/docs/src/main/asciidoc/security-oauth2.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using OAuth2 RBAC include::_attributes.adoc[] :categories: security +:keywords: oauth :summary: This guide explains how your Quarkus application can utilize OAuth2 tokens to provide secured access to the Jakarta REST endpoints. :extension-name: Elytron Security OAuth2 diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc index 3babb1e0bcdd1f..ffc38d23a2e72d 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-oidc-code-flow-authentication-tutorial"] = Protect a web application by using OpenID Connect (OIDC) authorization code flow include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 1f4eff83f391e2..cc3f9dedbb3d32 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -518,10 +518,15 @@ For example, if you have Quarkus services deployed on the following two domains, [[token-state-manager]] -===== Customizing the cookie with TokenStateManager +==== Session cookie and default TokenStateManager -OIDC `CodeAuthenticationMechanism` uses the default `io.quarkus.oidc.TokenStateManager` interface implementation to keep the ID, access, and refresh tokens returned in the authorization code or to refresh grant responses in a session cookie. -This makes Quarkus OIDC endpoints completely stateless. +OIDC `CodeAuthenticationMechanism` uses the default `io.quarkus.oidc.TokenStateManager` interface implementation to keep the ID, access, and refresh tokens returned in the authorization code or refresh grant responses in an encrypted session cookie. + +It makes Quarkus OIDC endpoints completely stateless and it is recommended to follow this strategy in order to achieve the best scalability results. + +See <> and <> sections of this guide for alternative approaches where tokens can be stored in the database or other server-side storage, if you prefer and have good reasons for storing the token state on the server. + +You can configure the default `TokenStateManager` to avoid saving an access token in the session cookie and only keep ID and refresh tokens or ID token only. An access token is only required if the endpoint needs to: @@ -542,13 +547,12 @@ In such cases, use the `quarkus.oidc.token-state-manager.strategy` property to c |=== - -If your chosen cookie strategy combines tokens and generates a large session cookie value that is greater than 4KB, some browsers might not be able to handle such cookie sizes. +If your chosen session cookie strategy combines tokens and generates a large session cookie value that is greater than 4KB, some browsers might not be able to handle such cookie sizes. This can occur when the ID, access, and refresh tokens are JWT tokens and the selected strategy is `keep-all-tokens` or with ID and refresh tokens when the strategy is `id-refresh-token`. -To workaround this issue, you can set `quarkus.oidc.token-state-manager.split-tokens=true` to create a unique session token for each token. +To workaround this issue, you can set `quarkus.oidc.token-state-manager.split-tokens=true` to create a unique session token for each token. An alternative solution is to have the tokens saved in the database, see <> for more information. -`TokenStateManager` encrypts the tokens before storing them in the session cookie. -The following example shows how you configure `TokenStateManager` to split the tokens and encrypt them: +Default `TokenStateManager` encrypts the tokens before storing them in the session cookie. +The following example shows how you configure it to split the tokens and encrypt them: [source, properties] ---- @@ -563,7 +567,6 @@ quarkus.oidc.token-state-manager.encryption-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34Y The token encryption secret must be at least 32 characters long. If this key is not configured then either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.jwt.secret` will be hashed to create an encryption key. - Configure the `quarkus.oidc.token-state-manager.encryption-secret` property if Quarkus authenticates to the OpenId Connect Provider by using one of the following authentication methods: * mTLS @@ -573,8 +576,12 @@ Otherwise, a random key is generated, which can be problematic if the Quarkus ap You can disable token encryption in the session cookie by setting `quarkus.oidc.token-state-manager.encryption-required=false`. -Register your own `io.quarkus.oidc.TokenStateManager' implementation as an `@ApplicationScoped` CDI bean if you need to customize the way the tokens are associated with the session cookie. -For example, you may want to keep the tokens in a database and have only a database pointer stored in a session cookie. +[[custom-token-state-manager]] +==== Session cookie and custom TokenStateManager + +Register a custom `io.quarkus.oidc.TokenStateManager' implementation as an `@ApplicationScoped` CDI bean if you need to customize the way the tokens are associated with the session cookie. + +For example, you may want to keep the tokens in a cache cluster and have only a key stored in a session cookie. Note that this approach might introduce some challenges if you need to make the tokens available across multiple microservices nodes. Here is a simple example: @@ -631,7 +638,102 @@ public class CustomTokenStateManager implements TokenStateManager { } } ---- -//SJ: In next iteration, propose to recompose Logout information into a new concept topic + +See <> for the information about the default `TokenStateManager` storing the tokens in an encrypted session cookie. + +See <> for the information about the custom `TokenStatemanager` implementation provided by Quarkus. + +[[db-token-state-manager]] +==== Database TokenStateManager + +If you prefer to follow a stateful token storage strategy, then you can use a custom `TokenStateManager` provided by Quarkus to have your application storing tokens in a database, instead of storing them in an encrypted session cookie which is done by default, as described in the <> section. + +To use this feature, add the following extension to your project: + +:add-extension-extensions: oidc-db-token-state-manager +include::{includes}/devtools/extension-add.adoc[] + +This extension will replace the default `io.quarkus.oidc.TokenStateManager' with a database-based one. + +OIDC Database Token State Manager is using a Reactive SQL client under the hood to avoid blocking since the authentication is likely to happen on IO thread. + +Depending on your database, please include and configure exactly one xref:reactive-sql-clients.adoc[Reactive SQL client]. +Following Reactive SQL clients are supported: + +* Reactive MS SQL client +* Reactive MySQL client +* Reactive PostgreSQL client +* Reactive Oracle client +* Reactive DB2 client + +IMPORTANT: Your application is not required to switch to using the Reactive SQL client if it already uses Hibernate ORM with one of the JDBC driver extensions. + +Let's say you already have application that is using the Hibernate ORM extension together with a PostgreSQL JDBC Driver and your datasource is configured like this: + +[source, properties] +---- +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=quarkus_test +quarkus.datasource.password=quarkus_test +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test +---- + +Now, if you decided to use OIDC Database Token State Manager, you need to add following dependencies and set a reactive driver URL. + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-oidc-db-token-state-manager + + + io.quarkus + quarkus-reactive-pg-client + +---- + +[source, properties] +---- +quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test +---- + +And you are ready to go. + +By default, a database table used for storing tokens is created for you, however you can disable this option with the `quarkus.oidc.db-token-state-manager.create-database-table-if-not-exists` configuration property. +Should you want the Hibernate ORM extension to create this table instead, you simply need to include an Entity like this one: + +[source, java] +---- +package org.acme.manager; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Table(name = "oidc_db_token_state_manager") <1> +@Entity +public class OidcDbTokenStateManagerEntity { + + @Id + String id; + + @Column(name = "id_token", length = 4000) <2> + String idToken; + + @Column(name = "refresh_token", length = 4000) + String refreshToken; + + @Column(name = "access_token", length = 4000) + String accessToken; + + @Column(name = "expires_in") + Long expiresIn; +} +---- +<1> The Hibernate ORM extension will only create this table for you when database schema is generated. Please refer to the xref:hibernate-orm.adoc[Hibernate ORM] guide for more information. +<2> You can choose column length depending on the length of your tokens. ==== Logout and expiration diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 0363757984663b..f875bc2f8931a7 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = OpenID Connect (OIDC) and OAuth2 Client and Filters Reference Guide - include::_attributes.adoc[] This reference guide explains how to use: diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index e63296fa17f516..f91f0c0671348f 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services and UI for OpenID Connect (OIDC) include::_attributes.adoc[] :categories: security +:keywords: sso oidc security keycloak :summary: Start Keycloak or other providers automatically in dev and test modes. This guide covers the Dev Services and UI for OpenID Connect (OIDC) Keycloak provider and explains how to support Dev Services and UI for other OpenID Connect providers. diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index 95a0f492ff7ca6..58034abdb8603d 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using OpenID Connect (OIDC) Multi-Tenancy include::_attributes.adoc[] :categories: security +:keywords: sso oidc oauth2 security :summary: This guide demonstrates how your OpenID Connect application can support multi-tenancy so that you can serve multiple tenants from a single application. This guide demonstrates how your OpenID Connect (OIDC) application can support multi-tenancy so that you can serve multiple tenants from a single application. Tenants can be distinct realms or security domains within the same OpenID Provider or even distinct OpenID Providers. @@ -590,9 +591,9 @@ To configure the resolution of the tenant identifier, use one of the following o [[default-tenant-resolver]] === Default resolution -The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in the last segment of the request path. +The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in the last segment of the request path. -The following `application.properties` example shows how you can configure two tenants named `google` and `github`: +The following `application.properties` example shows how you can configure two tenants named `google` and `github`: [source,properties] ---- @@ -609,7 +610,7 @@ quarkus.oidc.github.credentials.secret=${github-client-secret} quarkus.oidc.github.authentication.redirect-path=/signed-in ---- -In this example, both tenants configure OIDC `web-app` applications to use an authorization code flow to authenticate users and also require session cookies to get generated after the authentication has taken place. +In this example, both tenants configure OIDC `web-app` applications to use an authorization code flow to authenticate users and also require session cookies to get generated after the authentication has taken place. After either Google or GitHub authenticates the current user, the user gets returned to the `/signed-in` area for authenticated users, for example, a secured resource path on the JAX-RS endpoint. Finally, to complete the default tenant resolution, set the following configuration property: @@ -620,8 +621,8 @@ quarkus.http.auth.permission.login.paths=/google,/github quarkus.http.auth.permission.login.policy=authenticated ---- -If the endpoint is running on `http://localhost:8080`, you can also provide UI options for users to log in to either `http://localhost:8080/google` or `http://localhost:8080/github`, without having to add specific`/google` or `/github` JAX-RS resource paths. -Tenant identifiers are also recorded in the session cookie names after the authentication is completed. +If the endpoint is running on `http://localhost:8080`, you can also provide UI options for users to log in to either `http://localhost:8080/google` or `http://localhost:8080/github`, without having to add specific`/google` or `/github` JAX-RS resource paths. +Tenant identifiers are also recorded in the session cookie names after the authentication is completed. Therefore, authenticated users can access the secured application area without requiring either the `google` or `github` path values to be included in the secured URL. Default resolution can also work for Bearer token authentication but it might be less practical in this case because a tenant identifier will always need to be set as the last path segment value. @@ -629,7 +630,7 @@ Default resolution can also work for Bearer token authentication but it might be [[tenant-resolver]] === Resolve with `TenantResolver` -The following `application.properties` example shows how you can resolve the tenant identifier of two tenants named `a` and `b` by using the `TenantResolver` method: +The following `application.properties` example shows how you can resolve the tenant identifier of two tenants named `a` and `b` by using the `TenantResolver` method: [source,properties] ---- diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 5b253ec5455dce..395ab748fa0b4c 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -8,6 +8,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :diataxis-type: concept :categories: security,web +:keywords: oidc github twitter google facebook mastodon microsoft apple spotify twitch :toclevels: 3 This document explains how to configure well-known social OIDC and OAuth2 providers. @@ -211,7 +212,7 @@ quarkus.oidc.token.customizer-name=azure-access-token-customizer [[apple]] === Apple -In order to set up OIDC for Apple you need to create a developer account, and sign up for the 99€/year program, but you cannot test your application on `localhost` like most other OIDC providers: +In order to set up OIDC for Apple you need to create a developer account, and sign up for the 99€/year program, but you cannot test your application on `localhost` like most other OIDC providers: you will need to run it over `https` and make it publicly accessible, so for development purposes you may want to use a service such as https://ngrok.com. @@ -320,7 +321,7 @@ quarkus.oidc.credentials.jwt.subject=> for more information about registering your Quarkus appli === Spotify Create a https://developer.spotify.com/documentation/general/guides/authorization/app-settings/[Spotify application]: - + image::oidc-spotify-1.png[role="thumb"] Don't forget to add `http://localhost:8080` as a redirect URI for testing during development purposes. You should get a client id and secret generated once a Spotify application setup has been complete, for example: @@ -466,7 +467,7 @@ quarkus.oidc.credentials.secret= === Twitch Create a https://dev.twitch.tv/console/apps[Twitch application]: - + image::oidc-twitch-1.png[role="thumb"] You can now configure your `application.properties`: @@ -519,7 +520,7 @@ For more information, see the Quarkus xref:security-openid-connect-multitenancy. Sometimes, only authenticating users with a social provider is not enough. A provider-specific service also needs to be accessed for the Quarkus OIDC `web-app` application to fetch or update data from the provider service on behalf of the currently authenticated user. -As mentioned in the xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] guide, ID and access tokens are returned after the authorization code flow has been completed, with some providers like `GitHub` returning an access token only. +As mentioned in the xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] guide, ID and access tokens are returned after the authorization code flow has been completed, with some providers like `GitHub` returning an access token only. It is this access token that has to be propagated to services such as `Google Calendar`, or `Spotify Playlists` for the currently authenticated user to be able to use such services. You do not have to bring provider-specific libraries in order to achieve this, but instead you can use a reactive `Token Propagation` filter, which can be bound to a given REST client with a simple annotation. diff --git a/docs/src/main/asciidoc/security-overview.adoc b/docs/src/main/asciidoc/security-overview.adoc index 35d946c403e099..fac303f37048cb 100644 --- a/docs/src/main/asciidoc/security-overview.adoc +++ b/docs/src/main/asciidoc/security-overview.adoc @@ -1,3 +1,8 @@ +//// +This document is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// [id="security-overview"] = Quarkus Security overview include::_attributes.adoc[] diff --git a/docs/src/main/asciidoc/security-testing.adoc b/docs/src/main/asciidoc/security-testing.adoc index b034619ddeba3b..629f9f3cd8aab8 100644 --- a/docs/src/main/asciidoc/security-testing.adoc +++ b/docs/src/main/asciidoc/security-testing.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Security Testing - include::_attributes.adoc[] This document describes how to test Quarkus Security. diff --git a/docs/src/main/asciidoc/smallrye-metrics.adoc b/docs/src/main/asciidoc/smallrye-metrics.adoc index 5ea8d492c0664b..1ff12d3cbeadfd 100644 --- a/docs/src/main/asciidoc/smallrye-metrics.adoc +++ b/docs/src/main/asciidoc/smallrye-metrics.adoc @@ -3,7 +3,6 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// - = SmallRye Metrics :extension-status: deprecated include::_attributes.adoc[] @@ -177,7 +176,7 @@ The application will respond that 350 is not a prime number because it can be di curl localhost:8080/629521085409773 ---- + -The application will respond that 629521085409773 is a prime number. +The application will respond that 629521085409773 is a prime number. .. Perform additional calls with numbers of your choice. diff --git a/docs/src/main/asciidoc/stork-kubernetes.adoc b/docs/src/main/asciidoc/stork-kubernetes.adoc index dce9a2509f9782..47d9cdd41b8eae 100644 --- a/docs/src/main/asciidoc/stork-kubernetes.adoc +++ b/docs/src/main/asciidoc/stork-kubernetes.adoc @@ -5,7 +5,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Using Stork with Kubernetes :extension-status: preview - include::_attributes.adoc[] This guide explains how to use Stork with Kubernetes for service discovery and load balancing. diff --git a/docs/src/main/asciidoc/stork-reference.adoc b/docs/src/main/asciidoc/stork-reference.adoc index 0b1b477171d26b..b91c726ee0072e 100644 --- a/docs/src/main/asciidoc/stork-reference.adoc +++ b/docs/src/main/asciidoc/stork-reference.adoc @@ -5,7 +5,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Stork Reference Guide :extension-status: preview - include::_attributes.adoc[] This guide is the companion from the xref:stork.adoc[Stork Getting Started Guide]. diff --git a/docs/src/main/asciidoc/stork.adoc b/docs/src/main/asciidoc/stork.adoc index b280b34f92bf06..daf837aa4c3314 100644 --- a/docs/src/main/asciidoc/stork.adoc +++ b/docs/src/main/asciidoc/stork.adoc @@ -5,7 +5,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Getting Started with SmallRye Stork :extension-status: preview - include::_attributes.adoc[] The essence of distributed systems resides in the interaction between services. diff --git a/extensions/oidc-db-token-state-manager/deployment/pom.xml b/extensions/oidc-db-token-state-manager/deployment/pom.xml new file mode 100644 index 00000000000000..f05a8b5b250fca --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/pom.xml @@ -0,0 +1,99 @@ + + + + quarkus-oidc-db-token-state-manager-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-oidc-db-token-state-manager-deployment + Quarkus - OpenID Connect Database Token State Manager - Deployment + + + + io.quarkus + quarkus-oidc-db-token-state-manager + + + io.quarkus + quarkus-oidc-deployment + + + + io.quarkus + quarkus-resteasy-reactive-deployment + test + + + io.quarkus + quarkus-hibernate-orm-deployment + test + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + net.sourceforge.htmlunit + htmlunit + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + maven-surefire-plugin + + true + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${mssql.image} + ${db2.image} + + + + + + + + diff --git a/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerBuildTimeConfig.java b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerBuildTimeConfig.java new file mode 100644 index 00000000000000..729c87aa9887b2 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerBuildTimeConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.db.token.state.manager; + +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.oidc.db-token-state-manager") +@ConfigRoot +public interface OidcDbTokenStateManagerBuildTimeConfig { + + /** + * Whether token state should be stored in the database. + */ + @WithDefault("true") + boolean enabled(); + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java new file mode 100644 index 00000000000000..82af608de6dcba --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java @@ -0,0 +1,200 @@ +package io.quarkus.oidc.db.token.state.manager; + +import static io.quarkus.deployment.Capability.REACTIVE_DB2_CLIENT; +import static io.quarkus.deployment.Capability.REACTIVE_MSSQL_CLIENT; +import static io.quarkus.deployment.Capability.REACTIVE_MYSQL_CLIENT; +import static io.quarkus.deployment.Capability.REACTIVE_ORACLE_CLIENT; +import static io.quarkus.deployment.Capability.REACTIVE_PG_CLIENT; +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static java.lang.String.format; + +import java.util.function.BooleanSupplier; + +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Singleton; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.oidc.TokenStateManager; +import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManager; +import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerInitializer; +import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerInitializer.OidcDbTokenStateManagerInitializerProperties; +import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerRecorder; +import io.quarkus.runtime.configuration.ConfigurationException; + +@BuildSteps(onlyIf = OidcDbTokenStateManagerProcessor.OidcDbTokenStateManagerEnabled.class) +public class OidcDbTokenStateManagerProcessor { + + private static final String[] SUPPORTED_REACTIVE_CLIENTS = new String[] { REACTIVE_PG_CLIENT, REACTIVE_MYSQL_CLIENT, + REACTIVE_MSSQL_CLIENT, REACTIVE_DB2_CLIENT, REACTIVE_ORACLE_CLIENT }; + + @Record(STATIC_INIT) + @BuildStep + SyntheticBeanBuildItem produceDbTokenStateManagerBean(OidcDbTokenStateManagerRecorder recorder, + ReactiveSqlClientBuildItem sqlClientBuildItem) { + final String[] queryParamPlaceholders; + switch (sqlClientBuildItem.reactiveClient) { + case REACTIVE_PG_CLIENT: + queryParamPlaceholders = new String[] { "$1", "$2", "$3", "$4", "$5" }; + break; + case REACTIVE_MSSQL_CLIENT: + queryParamPlaceholders = new String[] { "@p1", "@p2", "@p3", "@p4", "@p5" }; + break; + case REACTIVE_MYSQL_CLIENT: + case REACTIVE_DB2_CLIENT: + case REACTIVE_ORACLE_CLIENT: + queryParamPlaceholders = new String[] { "?", "?", "?", "?", "?" }; + break; + default: + throw new RuntimeException("Unknown Reactive Sql Client " + sqlClientBuildItem.reactiveClient); + } + String deleteStatement = format("DELETE FROM oidc_db_token_state_manager WHERE id = %s", queryParamPlaceholders[0]); + String getQuery = format("SELECT id_token, access_token, refresh_token FROM oidc_db_token_state_manager WHERE " + + "id = %s", queryParamPlaceholders[0]); + String insertStatement = format("INSERT INTO oidc_db_token_state_manager (id_token, access_token, refresh_token," + + " expires_in, id) VALUES (%s, %s, %s, %s, %s)", queryParamPlaceholders[0], queryParamPlaceholders[1], + queryParamPlaceholders[2], queryParamPlaceholders[3], queryParamPlaceholders[4]); + return SyntheticBeanBuildItem + .configure(OidcDbTokenStateManager.class) + .alternative(true) + .priority(1) + .addType(TokenStateManager.class) + .unremovable() + .scope(Singleton.class) + .supplier(recorder.createTokenStateManager(insertStatement, deleteStatement, getQuery)) + .done(); + } + + @BuildStep + ReactiveSqlClientBuildItem validateReactiveSqlClient( + BuildProducer validationErrors, + Capabilities capabilities) { + ReactiveSqlClientBuildItem sqlClientDbTable = null; + for (String reactiveClient : SUPPORTED_REACTIVE_CLIENTS) { + if (capabilities.isPresent(reactiveClient)) { + if (sqlClientDbTable == null) { + sqlClientDbTable = new ReactiveSqlClientBuildItem(reactiveClient); + } else { + validationErrors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( + new ConfigurationException("The OpenID Connect Database Token State Manager extension is " + + "only supported when exactly one Reactive SQL Client extension is present."))); + return null; + } + } + } + if (sqlClientDbTable == null) { + validationErrors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(new ConfigurationException( + "The OpenID Connect Database Token State Manager extension requires Reactive SQL Client extension. " + + "Please refer to the https://quarkus.io/guides/reactive-sql-clients for more information."))); + } + return sqlClientDbTable; + } + + @BuildStep + AdditionalBeanBuildItem makeDbTokenStateManagerInitializerBean() { + return new AdditionalBeanBuildItem(OidcDbTokenStateManagerInitializer.class); + } + + @BuildStep + @Record(STATIC_INIT) + SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuildItem sqlClientBuildItem, + OidcDbTokenStateManagerRecorder recorder) { + final String createTableDdl; + final boolean supportsIfTableNotExists; + switch (sqlClientBuildItem.reactiveClient) { + case REACTIVE_PG_CLIENT: + createTableDdl = "CREATE TABLE IF NOT EXISTS oidc_db_token_state_manager (" + + "id VARCHAR(100) PRIMARY KEY, " + + "id_token VARCHAR, " + + "access_token VARCHAR, " + + "refresh_token VARCHAR, " + + "expires_in BIGINT NOT NULL)"; + supportsIfTableNotExists = true; + break; + case REACTIVE_MYSQL_CLIENT: + createTableDdl = "CREATE TABLE IF NOT EXISTS oidc_db_token_state_manager (" + + "id VARCHAR(100), " + + "id_token VARCHAR(5000) NULL, " + + "access_token VARCHAR(5000) NULL, " + + "refresh_token VARCHAR(5000) NULL, " + + "expires_in BIGINT NOT NULL, " + + "PRIMARY KEY (id))"; + supportsIfTableNotExists = true; + break; + case REACTIVE_MSSQL_CLIENT: + createTableDdl = "CREATE TABLE oidc_db_token_state_manager (" + + "id NVARCHAR(100) PRIMARY KEY, " + + "id_token NVARCHAR(MAX), " + + "access_token NVARCHAR(MAX), " + + "refresh_token NVARCHAR(MAX), " + + "expires_in BIGINT NOT NULL)"; + supportsIfTableNotExists = false; + break; + case REACTIVE_DB2_CLIENT: + createTableDdl = "CREATE TABLE oidc_db_token_state_manager (" + + "id VARCHAR(100) NOT NULL PRIMARY KEY, " + + "id_token VARCHAR(4000), " + + "access_token VARCHAR(4000), " + + "refresh_token VARCHAR(4000), " + + "expires_in BIGINT NOT NULL)"; + supportsIfTableNotExists = false; + break; + case REACTIVE_ORACLE_CLIENT: + createTableDdl = "CREATE TABLE IF NOT EXISTS oidc_db_token_state_manager (" + + "id VARCHAR2(100), " + + "id_token VARCHAR2(4000), " + + "access_token VARCHAR2(4000), " + + "refresh_token VARCHAR2(4000), " + + "expires_in NUMBER NOT NULL, " + + "PRIMARY KEY (id))"; + supportsIfTableNotExists = true; + break; + default: + throw new ConfigurationException("Unknown Reactive Sql Client " + sqlClientBuildItem.reactiveClient); + } + return SyntheticBeanBuildItem + .configure(OidcDbTokenStateManagerInitializerProperties.class) + .supplier(recorder.createDbTokenStateInitializerProps(createTableDdl, supportsIfTableNotExists)) + .unremovable() + .scope(Dependent.class) + .done(); + } + + @Consume(SyntheticBeansRuntimeInitBuildItem.class) + @Record(RUNTIME_INIT) + @BuildStep + void setSqlClientPool(OidcDbTokenStateManagerRecorder recorder, BeanContainerBuildItem beanContainer) { + recorder.setSqlClientPool(beanContainer.getValue()); + } + + static final class OidcDbTokenStateManagerEnabled implements BooleanSupplier { + + OidcDbTokenStateManagerBuildTimeConfig config; + + @Override + public boolean getAsBoolean() { + return config.enabled(); + } + } + + static final class ReactiveSqlClientBuildItem extends SimpleBuildItem { + + private final String reactiveClient; + + private ReactiveSqlClientBuildItem(String reactiveClient) { + this.reactiveClient = reactiveClient; + } + } + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java new file mode 100644 index 00000000000000..e506b2f63d4fcb --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java @@ -0,0 +1,105 @@ +package io.quarkus.oidc.db.token.state.manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.function.Consumer; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; + +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.TextPage; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.restassured.RestAssured; + +public abstract class AbstractDbTokenStateManagerTest { + + protected static QuarkusUnitTest createQuarkusUnitTest(String reactiveSqlClientExtension) { + return createQuarkusUnitTest(reactiveSqlClientExtension, null); + } + + protected static QuarkusUnitTest createQuarkusUnitTest(String reactiveSqlClientExtension, + Consumer customizer) { + return new QuarkusUnitTest() + .withApplicationRoot((jar) -> { + jar + .addClasses(ProtectedResource.class, UnprotectedResource.class, PublicResource.class) + .addAsResource("application.properties"); + if (customizer != null) { + customizer.accept(jar); + } + }) + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", reactiveSqlClientExtension, Version.getVersion()))); + } + + @TestHTTPResource + URL url; + + @Test + public void testCodeFlow() throws IOException { + + try (final WebClient webClient = createWebClient()) { + + TextPage textPage = webClient.getPage(url.toString() + "unprotected"); + assertEquals("unprotected", textPage.getContent()); + + HtmlPage page; + page = webClient.getPage(url.toString() + "protected"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + textPage = loginForm.getInputByName("login").click(); + + assertEquals("alice", textPage.getContent()); + + assertTokenStateCount(1); + + webClient.getOptions().setRedirectEnabled(false); + WebResponse webResponse = webClient + .loadWebResponse(new WebRequest(URI.create(url.toString() + "protected/logout").toURL())); + assertEquals(302, webResponse.getStatusCode()); + assertNull(webClient.getCookieManager().getCookie("q_session")); + + webClient.getCookieManager().clearCookies(); + + assertTokenStateCount(0); + } + } + + protected static void assertTokenStateCount(Integer tokenStateCount) { + RestAssured + .given() + .get("public/db-state-manager-table-content") + .then() + .statusCode(200) + .body(Matchers.is(tokenStateCount.toString())); + } + + protected static WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/Db2DbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/Db2DbTokenStateManagerTest.java new file mode 100644 index 00000000000000..84e87744cdf6c9 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/Db2DbTokenStateManagerTest.java @@ -0,0 +1,20 @@ +package io.quarkus.oidc.db.token.state.manager; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +// TODO: this test works and we simply need to run it, however in CI it is going to hit +// hang detection timeout set by 'quarkus.test.hang-detection-timeout=60', we need to discuss and try +// to find a way to run it (like allow QuarkusUnitTests to override system property etc.) +// but it will require separate PR and make changes unrelated to DB Token State Manager +@EnabledIfSystemProperty(named = "run-db2-db-token-state-manager-test", disabledReason = "Db2 is slow to start", matches = "true") +public class Db2DbTokenStateManagerTest extends AbstractDbTokenStateManagerTest { + + @RegisterExtension + static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-db2-client", + jar -> jar.addAsResource(new StringAsset(System.getProperty("db2.image")), "container-license-acceptance.txt")); + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingEntity.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingEntity.java new file mode 100644 index 00000000000000..1ad66681f11acf --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingEntity.java @@ -0,0 +1,18 @@ +package io.quarkus.oidc.db.token.state.manager; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Table(name = "greeting") +@Entity +public class GreetingEntity { + + @Id + @GeneratedValue + Long id; + + String greeting; + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingResource.java new file mode 100644 index 00000000000000..5d9feec729ac5c --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingResource.java @@ -0,0 +1,36 @@ +package io.quarkus.oidc.db.token.state.manager; + +import java.util.Objects; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.security.Authenticated; + +@Path("/greeting") +@Authenticated +public class GreetingResource { + + @Inject + EntityManager em; + + @Transactional + @Path("/new") + @GET + public void newGreeting() { + var entity = new GreetingEntity(); + entity.greeting = Objects.requireNonNull("Good day"); + em.persist(entity); + } + + @GET + public Object getGreetings() { + return em + .createNativeQuery("SELECT greeting FROM Greeting") + .getResultList() + .get(0); + } +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java new file mode 100644 index 00000000000000..eaa9390090ae4a --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java @@ -0,0 +1,92 @@ +package io.quarkus.oidc.db.token.state.manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.List; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class HibernateOrmPgDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ProtectedResource.class, UnprotectedResource.class, PublicResource.class, + GreetingResource.class, GreetingEntity.class, OidcDbTokenStateManagerEntity.class, + OidcDbTokenStateManagerResource.class) + .addAsResource("hibernate-orm-application.properties", "application.properties")) + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-reactive-pg-client", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-jdbc-postgresql", Version.getVersion()))); + + @Test + public void testCodeFlowOnTableNotCreatedByExtension() throws IOException { + // also tests that this extension works with Hibernate ORM (creates / updates entity) + try (final WebClient webClient = createWebClient()) { + HtmlPage page; + page = webClient.getPage(url.toString() + "greeting/new"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + page = loginForm.getInputByName("login").click(); + assertEquals(200, page.getWebResponse().getStatusCode()); + + WebResponse webResponse = webClient.loadWebResponse( + new WebRequest(URI.create(url.toString() + "greeting").toURL())); + assertEquals(200, webResponse.getStatusCode()); + assertTrue(webResponse.getContentAsString().contains("Good day")); + + webClient.getOptions().setRedirectEnabled(false); + webResponse = webClient + .loadWebResponse(new WebRequest(URI.create(url.toString() + "protected/logout").toURL())); + assertEquals(302, webResponse.getStatusCode()); + assertNull(webClient.getCookieManager().getCookie("q_session")); + + webClient.getCookieManager().clearCookies(); + } + } + + @Test + public void testExpiredTokenDeletion() { + assertTokenStateCount(0); + + // create 3 tokens + RestAssured + .given() + .body(3) + .post("/token-state-manager-generator") + .then() + .statusCode(204); + assertTokenStateCount(3); + + // make sure expired tokens are deleted + Awaitility + .await() + .ignoreExceptions() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> assertTokenStateCount(0)); + } +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MsSqlDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MsSqlDbTokenStateManagerTest.java new file mode 100644 index 00000000000000..846e020bf35b1e --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MsSqlDbTokenStateManagerTest.java @@ -0,0 +1,14 @@ +package io.quarkus.oidc.db.token.state.manager; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MsSqlDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest { + + @RegisterExtension + static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-mssql-client", + jar -> jar.addAsResource(new StringAsset(System.getProperty("mssql.image")), "container-license-acceptance.txt")); + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MySqlDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MySqlDbTokenStateManagerTest.java new file mode 100644 index 00000000000000..3a729ac393a785 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MySqlDbTokenStateManagerTest.java @@ -0,0 +1,12 @@ +package io.quarkus.oidc.db.token.state.manager; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MySqlDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest { + + @RegisterExtension + static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-mysql-client"); + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java new file mode 100644 index 00000000000000..ce09a849c551a5 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java @@ -0,0 +1,26 @@ +package io.quarkus.oidc.db.token.state.manager; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Table(name = "oidc_db_token_state_manager") +@Entity +public class OidcDbTokenStateManagerEntity { + + @Id + String id; + + @Column(name = "id_token", length = 4000) + String idToken; + + @Column(name = "refresh_token", length = 4000) + String refreshToken; + + @Column(name = "access_token", length = 4000) + String accessToken; + + @Column(name = "expires_in") + Long expiresIn; +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerResource.java new file mode 100644 index 00000000000000..c0256f2dc8909a --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerResource.java @@ -0,0 +1,35 @@ +package io.quarkus.oidc.db.token.state.manager; + +import java.time.Instant; +import java.util.UUID; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +@Path("/token-state-manager-generator") +public class OidcDbTokenStateManagerResource { + + private static final long EXPIRED_EXTRA_GRACE = 30; + + @Inject + EntityManager em; + + @Transactional + @POST + public void create(Long numOfTokens) { + long expiresIn5Sec = Instant.now().getEpochSecond() + 5 - EXPIRED_EXTRA_GRACE; + for (int i = 0; i < numOfTokens; i++) { + var token = new OidcDbTokenStateManagerEntity(); + token.idToken = "ID TOKEN " + i; + token.accessToken = "ACCESS TOKEN " + i; + token.refreshToken = "REFRESH TOKEN " + i; + token.expiresIn = expiresIn5Sec; + token.id = UUID.randomUUID().toString() + Instant.now().getEpochSecond(); + em.persist(token); + } + } + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PostgresDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PostgresDbTokenStateManagerTest.java new file mode 100644 index 00000000000000..c38ad37596d669 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PostgresDbTokenStateManagerTest.java @@ -0,0 +1,12 @@ +package io.quarkus.oidc.db.token.state.manager; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class PostgresDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest { + + @RegisterExtension + static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-pg-client"); + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java new file mode 100644 index 00000000000000..c47c7384853cec --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java @@ -0,0 +1,31 @@ +package io.quarkus.oidc.db.token.state.manager; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; + +@Path("/protected") +@Authenticated +public class ProtectedResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @GET + public String getName() { + return idToken.getName(); + } + + @GET + @Path("logout") + public void logout() { + throw new RuntimeException("Logout must be handled by CodeAuthenticationMechanism"); + } + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PublicResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PublicResource.java new file mode 100644 index 00000000000000..e0d7b1c4633d88 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PublicResource.java @@ -0,0 +1,44 @@ +package io.quarkus.oidc.db.token.state.manager; + +import java.util.function.Function; + +import javax.annotation.security.PermitAll; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.smallrye.mutiny.Uni; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; + +@Path("/public") +@PermitAll +public class PublicResource { + + @Inject + Pool pool; + + @Path("/db-state-manager-table-content") + @GET + public Uni getDbStateManagerRowsCount() { + return Uni.createFrom().completionStage(pool + .query("SELECT COUNT(*) FROM oidc_db_token_state_manager") + .execute() + .map(new Function, Long>() { + @Override + public Long apply(RowSet rows) { + if (rows != null) { + var iterator = rows.iterator(); + if (iterator.hasNext()) { + return iterator.next().getLong(0); + } + } + return 0L; + } + }) + .toCompletionStage()); + } + +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/UnprotectedResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/UnprotectedResource.java new file mode 100644 index 00000000000000..830c5a56892f54 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/UnprotectedResource.java @@ -0,0 +1,13 @@ +package io.quarkus.oidc.db.token.state.manager; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/unprotected") +public class UnprotectedResource { + + @GET + public String getName() { + return "unprotected"; + } +} diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties new file mode 100644 index 00000000000000..3d1deb0eee114c --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties @@ -0,0 +1,7 @@ +quarkus.oidc.client-id=quarkus-web-app +quarkus.oidc.application-type=web-app +quarkus.oidc.logout.path=/protected/logout +quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."com.gargoylesoftware.htmlunit.css".level=FATAL +quarkus.hibernate-orm.enabled=false +quarkus.datasource.jdbc=false diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties new file mode 100644 index 00000000000000..5e07052d84a6cd --- /dev/null +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties @@ -0,0 +1,7 @@ +quarkus.oidc.client-id=quarkus-web-app +quarkus.oidc.application-type=web-app +quarkus.oidc.logout.path=/protected/logout +quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."com.gargoylesoftware.htmlunit.css".level=FATAL +quarkus.oidc.db-token-state-manager.delete-expired-delay=3 +quarkus.oidc.db-token-state-manager.create-database-table-if-not-exists=false diff --git a/extensions/oidc-db-token-state-manager/pom.xml b/extensions/oidc-db-token-state-manager/pom.xml new file mode 100644 index 00000000000000..1e7429381d95f3 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-oidc-db-token-state-manager-parent + Quarkus - OpenID Connect Database Token State Manager - Parent + pom + + deployment + runtime + + diff --git a/extensions/oidc-db-token-state-manager/runtime/pom.xml b/extensions/oidc-db-token-state-manager/runtime/pom.xml new file mode 100644 index 00000000000000..4b42f1dd76bde5 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/runtime/pom.xml @@ -0,0 +1,46 @@ + + + + quarkus-oidc-db-token-state-manager-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-oidc-db-token-state-manager + Quarkus - OpenID Connect Database Token State Manager - Runtime + Store an OpenID Connect token state in a database + + + io.quarkus + quarkus-oidc + + + io.vertx + vertx-sql-client + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java new file mode 100644 index 00000000000000..adb550e821170d --- /dev/null +++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java @@ -0,0 +1,141 @@ +package io.quarkus.oidc.db.token.state.manager.runtime; + +import static io.quarkus.oidc.runtime.CodeAuthenticationMechanism.SESSION_MAX_AGE_PARAM; + +import java.time.Instant; +import java.util.UUID; +import java.util.function.Function; + +import org.jboss.logging.Logger; + +import io.quarkus.oidc.AuthorizationCodeTokens; +import io.quarkus.oidc.OidcRequestContext; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenStateManager; +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; + +public class OidcDbTokenStateManager implements TokenStateManager { + + private static final Logger LOG = Logger.getLogger(OidcDbTokenStateManager.class); + private static final String TOKEN_STATE_INSERT_FAILED = "Failed to insert token state into database"; + private static final String FAILED_TO_ACQUIRE_TOKEN = "Failed to acquire authorization code tokens"; + private final String insertStatement; + private final String deleteStatement; + private final String getQuery; + private Pool pool; + + OidcDbTokenStateManager(String insertStatement, String deleteStatement, String getQuery) { + this.insertStatement = insertStatement; + this.deleteStatement = deleteStatement; + this.getQuery = getQuery; + } + + void setSqlClientPool(Pool pool) { + this.pool = pool; + } + + @Override + public Uni createTokenState(RoutingContext event, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens, OidcRequestContext requestContext) { + final String id = now() + UUID.randomUUID().toString(); + return Uni + .createFrom() + .completionStage( + pool + .withTransaction(client -> client + .preparedQuery(insertStatement) + .execute( + Tuple.of(tokens.getIdToken(), tokens.getAccessToken(), + tokens.getRefreshToken(), expiresIn(event), id))) + .toCompletionStage()) + .onFailure().transform(new Function() { + @Override + public Throwable apply(Throwable throwable) { + return new AuthenticationFailedException(TOKEN_STATE_INSERT_FAILED, throwable); + } + }) + .flatMap(new Function, Uni>() { + @Override + public Uni apply(RowSet rows) { + if (rows != null) { + return Uni.createFrom().item(id); + } + return Uni.createFrom().failure(new AuthenticationFailedException(TOKEN_STATE_INSERT_FAILED)); + } + }) + .memoize().indefinitely(); + } + + @Override + public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + OidcRequestContext requestContext) { + return Uni + .createFrom() + .completionStage( + pool + .preparedQuery(getQuery) + .execute(Tuple.of(tokenState)) + .toCompletionStage()) + .onFailure().transform(new Function() { + @Override + public Throwable apply(Throwable throwable) { + return new AuthenticationCompletionException(FAILED_TO_ACQUIRE_TOKEN, throwable); + } + }) + .flatMap(new Function, Uni>() { + @Override + public Uni apply(RowSet rows) { + if (rows != null) { + final RowIterator iterator = rows.iterator(); + if (iterator.hasNext()) { + final Row firstRow = iterator.next(); + return Uni + .createFrom() + .item(new AuthorizationCodeTokens( + firstRow.getString("id_token"), + firstRow.getString("access_token"), + firstRow.getString("refresh_token"))); + } + } + return Uni.createFrom().failure(new AuthenticationCompletionException(FAILED_TO_ACQUIRE_TOKEN)); + } + }) + .memoize().indefinitely(); + } + + @Override + public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + OidcRequestContext requestContext) { + return Uni + .createFrom() + .completionStage(pool + .preparedQuery(deleteStatement) + .execute(Tuple.of(tokenState)) + .toCompletionStage()) + .replaceWithVoid() + .onFailure() + .recoverWithItem(new Function() { + @Override + public Void apply(Throwable throwable) { + LOG.debugf("Failed to delete tokens: %s", throwable.getMessage()); + return null; + } + }); + } + + static long now() { + return Instant.now().getEpochSecond(); + } + + private static long expiresIn(RoutingContext event) { + return now() + event. get(SESSION_MAX_AGE_PARAM); + } +} diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerInitializer.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerInitializer.java new file mode 100644 index 00000000000000..0f4fce54ddbf0f --- /dev/null +++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerInitializer.java @@ -0,0 +1,145 @@ +package io.quarkus.oidc.db.token.state.manager.runtime; + +import static io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManager.now; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import jakarta.enterprise.event.Observes; + +import org.jboss.logging.Logger; + +import io.quarkus.runtime.StartupEvent; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; + +public class OidcDbTokenStateManagerInitializer { + + private static final Logger LOG = Logger.getLogger(OidcDbTokenStateManagerInitializer.class); + private static final String FAILED_TO_CREATE_DB_TABLE = "unknown reason, please report the issue and create table manually"; + /** + * Extra 30 seconds before we delete expired tokens. + */ + private static final long EXPIRED_EXTRA_GRACE = 30; + + void initialize(@Observes StartupEvent event, OidcDbTokenStateManagerRunTimeConfig config, Vertx vertx, Pool pool, + OidcDbTokenStateManagerInitializerProperties initializerProps) { + if (config.createDatabaseTableIfNotExists()) { + createDatabaseTable(pool, initializerProps.createTableDdl, initializerProps.supportsIfTableNotExists); + } + periodicallyDeleteExpiredTokens(vertx, pool, config.deleteExpiredDelay().toMillis()); + } + + private static void periodicallyDeleteExpiredTokens(Vertx vertx, Pool pool, long delayBetweenChecks) { + vertx + .setPeriodic(5000, delayBetweenChecks, new Handler() { + + private final AtomicBoolean deleteInProgress = new AtomicBoolean(false); + + @Override + public void handle(Long aLong) { + if (deleteInProgress.compareAndSet(false, true)) { + + final long deleteExpiresIn = now() - EXPIRED_EXTRA_GRACE; + Uni.createFrom().completionStage( + pool + .query("DELETE FROM oidc_db_token_state_manager WHERE expires_in < " + + deleteExpiresIn) + .execute() + .toCompletionStage()) + .subscribe() + .with( + new Consumer>() { + @Override + public void accept(RowSet ignored) { + // success + deleteInProgress.set(false); + } + }, + new Consumer() { + @Override + public void accept(Throwable t) { + LOG.errorf("Failed to expired OIDC token states from database: %s", + t.getMessage()); + deleteInProgress.set(false); + } + }); + } + } + }); + } + + private static void createDatabaseTable(Pool pool, String createTableDdl, boolean supportsIfTableNotExists) { + LOG.debugf("Creating database table with query: %s", createTableDdl); + String errMsg = Uni + .createFrom() + .completionStage( + pool + .query(createTableDdl) + .execute() + .toCompletionStage()) + .onItemOrFailure() + .transformToUni(new BiFunction, Throwable, Uni>() { + @Override + public Uni apply(RowSet rows, Throwable throwable) { + if (throwable != null) { + if (supportsIfTableNotExists) { + return Uni.createFrom().item(throwable.getMessage()); + } else { + // most likely we tried to create table even though it already exists + return Uni.createFrom().nullItem(); + } + } + // assert table exists + return Uni + .createFrom() + .completionStage(pool + // use MAX in order to limit response size + // and LIMIT clause is not supported by all the databases + .query("SELECT MAX(id) FROM oidc_db_token_state_manager") + .execute() + .toCompletionStage()) + .map(new Function, String>() { + @Override + public String apply(RowSet rows) { + if (rows != null && rows.columnsNames().size() == 1) { + // table exists + return null; + } + // table does not exist + return FAILED_TO_CREATE_DB_TABLE; + } + }) + .onFailure().recoverWithItem(new Function() { + @Override + public String apply(Throwable throwable) { + LOG.error("Create database query failed with: ", throwable); + return FAILED_TO_CREATE_DB_TABLE; + } + }); + } + }) + .await() + .indefinitely(); + if (errMsg != null) { + throw new RuntimeException("OIDC Token State Manager failed to create database table: " + errMsg); + } + } + + public static final class OidcDbTokenStateManagerInitializerProperties { + + private final String createTableDdl; + private final boolean supportsIfTableNotExists; + + OidcDbTokenStateManagerInitializerProperties(String createTableDdl, boolean supportsIfTableNotExists) { + this.createTableDdl = createTableDdl; + this.supportsIfTableNotExists = supportsIfTableNotExists; + } + } +} diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRecorder.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRecorder.java new file mode 100644 index 00000000000000..3a69c6bf2dbb08 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRecorder.java @@ -0,0 +1,39 @@ +package io.quarkus.oidc.db.token.state.manager.runtime; + +import java.util.function.Supplier; + +import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerInitializer.OidcDbTokenStateManagerInitializerProperties; +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.sqlclient.Pool; + +@Recorder +public class OidcDbTokenStateManagerRecorder { + + /* STATIC INIT */ + public Supplier createTokenStateManager(String insertStatement, String deleteStatement, + String getQuery) { + return new Supplier() { + @Override + public OidcDbTokenStateManager get() { + return new OidcDbTokenStateManager(insertStatement, deleteStatement, getQuery); + } + }; + } + + /* RUNTIME INIT */ + public void setSqlClientPool(BeanContainer container) { + container.beanInstance(OidcDbTokenStateManager.class).setSqlClientPool(container.beanInstance(Pool.class)); + } + + /* STATIC INIT */ + public Supplier createDbTokenStateInitializerProps(String createTableDdl, + boolean supportsIfTableNotExists) { + return new Supplier() { + @Override + public OidcDbTokenStateManagerInitializerProperties get() { + return new OidcDbTokenStateManagerInitializerProperties(createTableDdl, supportsIfTableNotExists); + } + }; + } +} diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRunTimeConfig.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRunTimeConfig.java new file mode 100644 index 00000000000000..1e6136a0f64a9b --- /dev/null +++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRunTimeConfig.java @@ -0,0 +1,26 @@ +package io.quarkus.oidc.db.token.state.manager.runtime; + +import static io.quarkus.runtime.annotations.ConfigPhase.RUN_TIME; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.oidc.db-token-state-manager") +@ConfigRoot(phase = RUN_TIME) +public interface OidcDbTokenStateManagerRunTimeConfig { + + /** + * How often should Quarkus check for expired tokens. + */ + @WithDefault("8h") + Duration deleteExpiredDelay(); + + /** + * Whether Quarkus should attempt to create database table where the token state is going to be stored. + */ + @WithDefault("true") + boolean createDatabaseTableIfNotExists(); +} diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-db-token-state-manager/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..1497ca376c53e9 --- /dev/null +++ b/extensions/oidc-db-token-state-manager/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,16 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "OpenID Connect Database Token State Manager" +metadata: + keywords: + - "oauth2" + - "openid-connect" + - "oidc" + - "oidc-token" + - "oidc-db-token-state-manager" + guide: "https://quarkus.io/guides/security-openid-connect-client" + categories: + - "security" + status: "preview" + config: + - "quarkus.oidc.db-token-state-manager." 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 c98d7b23cf2d68..d40a81d4e67a4c 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 @@ -437,14 +437,14 @@ public enum Strategy { public boolean splitTokens; /** - * Mandates that the session cookie that stores the tokens is encrypted. + * Mandates that the Default TokenStateManager will encrypt the session cookie that stores the tokens. */ @ConfigItem(defaultValue = "true") public boolean encryptionRequired = true; /** - * Secret which will be used to encrypt the session cookie storing the tokens when {@link #encryptionRequired} property - * is enabled. + * Secret which will be used by the Default TokenStateManager to encrypt the session cookie + * storing the tokens when {@link #encryptionRequired} property is enabled. *

* If this secret is not set, the client secret configured with * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` will be checked. 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 be150ea8a97a0c..1dbf394b2da866 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 @@ -60,13 +60,13 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism { + public static final String SESSION_MAX_AGE_PARAM = "session-max-age"; static final String AMP = "&"; static final String EQ = "="; static final String COMMA = ","; static final String UNDERSCORE = "_"; static final String COOKIE_DELIM = "|"; static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM); - static final String SESSION_MAX_AGE_PARAM = "session-max-age"; static final String STATE_COOKIE_RESTORE_PATH = "restore-path"; static final Uni VOID_UNI = Uni.createFrom().voidItem(); static final Integer MAX_COOKIE_VALUE_LENGTH = 4096; diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 983da2a9121538..f263832b8d8105 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; @@ -19,12 +20,14 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider; +import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; @@ -42,6 +45,7 @@ import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.InterceptorBindingRegistrar; +import io.quarkus.arc.processor.Transformation; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -64,6 +68,7 @@ import io.quarkus.opentelemetry.runtime.config.build.ExporterType; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; +import io.quarkus.opentelemetry.runtime.tracing.cdi.AddingSpanAttributesInterceptor; import io.quarkus.opentelemetry.runtime.tracing.cdi.WithSpanInterceptor; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder; import io.quarkus.runtime.LaunchMode; @@ -76,8 +81,17 @@ public class OpenTelemetryProcessor { private static final DotName LEGACY_WITH_SPAN = DotName.createSimple( io.opentelemetry.extension.annotations.WithSpan.class.getName()); private static final DotName WITH_SPAN = DotName.createSimple(WithSpan.class.getName()); + private static final DotName ADD_SPAN_ATTRIBUTES = DotName.createSimple(AddingSpanAttributes.class.getName()); + private static final Predicate isAddSpanAttribute = new Predicate<>() { + @Override + public boolean test(AnnotationInstance annotationInstance) { + return annotationInstance.name().equals(ADD_SPAN_ATTRIBUTES); + } + }; private static final DotName SPAN_KIND = DotName.createSimple(SpanKind.class.getName()); private static final DotName WITH_SPAN_INTERCEPTOR = DotName.createSimple(WithSpanInterceptor.class.getName()); + private static final DotName ADD_SPAN_ATTRIBUTES_INTERCEPTOR = DotName + .createSimple(AddingSpanAttributesInterceptor.class.getName()); private static final DotName SPAN_ATTRIBUTE = DotName.createSimple(SpanAttribute.class.getName()); @BuildStep @@ -168,11 +182,15 @@ void registerWithSpan( new InterceptorBindingRegistrar() { @Override public List getAdditionalBindings() { - return List.of(InterceptorBinding.of(WithSpan.class, Set.of("value", "kind"))); + return List.of( + InterceptorBinding.of(WithSpan.class, Set.of("value", "kind")), + InterceptorBinding.of(AddingSpanAttributes.class, Set.of("value"))); } })); - additionalBeans.produce(new AdditionalBeanBuildItem(WithSpanInterceptor.class)); + additionalBeans.produce(new AdditionalBeanBuildItem( + WithSpanInterceptor.class, + AddingSpanAttributesInterceptor.class)); } @BuildStep @@ -209,11 +227,21 @@ public void transform(TransformationContext context) { annotationsTransformer.produce(new AnnotationsTransformerBuildItem(transformationContext -> { AnnotationTarget target = transformationContext.getTarget(); + Transformation transform = transformationContext.transform(); if (target.kind().equals(AnnotationTarget.Kind.CLASS)) { if (target.asClass().name().equals(WITH_SPAN_INTERCEPTOR)) { - transformationContext.transform().add(WITH_SPAN).done(); + transform.add(WITH_SPAN); + } else if (target.asClass().name().equals(ADD_SPAN_ATTRIBUTES_INTERCEPTOR)) { + transform.add(ADD_SPAN_ATTRIBUTES); + } + } else if (target.kind() == AnnotationTarget.Kind.METHOD) { + MethodInfo methodInfo = target.asMethod(); + // WITH_SPAN_INTERCEPTOR and ADD_SPAN_ATTRIBUTES must not be applied at the same time and the first has priority. + if (methodInfo.hasAnnotation(WITH_SPAN) && methodInfo.hasAnnotation(ADD_SPAN_ATTRIBUTES)) { + transform.remove(isAddSpanAttribute); } } + transform.done(); })); } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java new file mode 100644 index 00000000000000..9853edf6aa817f --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java @@ -0,0 +1,167 @@ +package io.quarkus.opentelemetry.deployment.interceptor; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class AddingSpanAttributesInterceptorTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClass(HelloRouter.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) + .addAsManifestResource( + "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource("resource-config/application.properties", "application.properties")); + + @Inject + HelloRouter helloRouter; + @Inject + Tracer tracer; + @Inject + TestSpanExporter spanExporter; + + @AfterEach + void tearDown() { + spanExporter.reset(); + } + + @Test + void withSpanAttributesTest_existingSpan() { + Span span = tracer.spanBuilder("withSpanAttributesTest").startSpan(); + String result; + try (Scope scope = span.makeCurrent()) { + result = helloRouter.withSpanAttributes( + "implicit", "explicit", null, "ignore"); + } finally { + span.end(); + } + assertEquals("hello!", result); + List spanItems = spanExporter.getFinishedSpanItems(1); + SpanData spanDataOut = spanItems.get(0); + assertEquals("withSpanAttributesTest", spanDataOut.getName()); + assertEquals(INTERNAL, spanDataOut.getKind()); + assertFalse(spanDataOut.getAttributes().isEmpty(), "No attributes found"); + assertEquals("implicit", getAttribute(spanDataOut, "implicitName")); + assertEquals("explicit", getAttribute(spanDataOut, "explicitName")); + } + + @Test + void withSpanAttributesTest_noActiveSpan() { + String resultWithoutSpan = helloRouter.withSpanAttributes( + "implicit", "explicit", null, "ignore"); + assertEquals("hello!", resultWithoutSpan); + + spanExporter.getFinishedSpanItems(0); + // No span created + + String resultWithSpan = helloRouter.withSpanTakesPrecedence( + "implicit", "explicit", null, "ignore"); + assertEquals("hello!", resultWithSpan); + + // we need 1 span to make sure we don't get a false positive. + // The previous call to getFinishedSpanItems might return too early. + + List spanItems = spanExporter.getFinishedSpanItems(1); + assertEquals(1, spanItems.size()); + SpanData spanDataOut = spanItems.get(0); + assertEquals("HelloRouter.withSpanTakesPrecedence", spanDataOut.getName()); + } + + @Test + void withSpanAttributesTest_newSpan() { + String result = helloRouter.withSpanTakesPrecedence( + "implicit", "explicit", null, "ignore"); + + assertEquals("hello!", result); + List spanItems = spanExporter.getFinishedSpanItems(1); + SpanData spanDataOut = spanItems.get(0); + assertEquals("HelloRouter.withSpanTakesPrecedence", spanDataOut.getName()); + assertEquals(INTERNAL, spanDataOut.getKind()); + assertEquals(2, spanDataOut.getAttributes().size()); + assertEquals("implicit", getAttribute(spanDataOut, "implicitName")); + assertEquals("explicit", getAttribute(spanDataOut, "explicitName")); + } + + @Test + void noAttributesAdded() { + Span span = tracer.spanBuilder("noAttributesAdded").startSpan(); + String result; + try (Scope scope = span.makeCurrent()) { + result = helloRouter.noAttributesAdded( + "implicit", "explicit", null, "ignore"); + } finally { + span.end(); + } + assertEquals("hello!", result); + List spanItems = spanExporter.getFinishedSpanItems(1); + SpanData spanDataOut = spanItems.get(0); + assertEquals("noAttributesAdded", spanDataOut.getName()); + assertEquals(INTERNAL, spanDataOut.getKind()); + assertTrue(spanDataOut.getAttributes().isEmpty(), "No attributes must be present"); + } + + private static Object getAttribute(SpanData spanDataOut, String attributeName) { + return spanDataOut.getAttributes().asMap().get(AttributeKey.stringKey(attributeName)); + } + + @ApplicationScoped + public static class HelloRouter { + // mast have already an active span + @AddingSpanAttributes + public String withSpanAttributes( + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + + @WithSpan + @AddingSpanAttributes + public String withSpanTakesPrecedence( + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + + public String noAttributesAdded( + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java similarity index 99% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java index 52de43f905ee15..b12624fd36912c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.interceptor; import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanLegacyInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java similarity index 99% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanLegacyInterceptorTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java index b6b2eb1f4f30e4..a38c18c57f6263 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanLegacyInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.interceptor; import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/AddingSpanAttributesInterceptor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/AddingSpanAttributesInterceptor.java new file mode 100644 index 00000000000000..3eecc0f3cade58 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/AddingSpanAttributesInterceptor.java @@ -0,0 +1,87 @@ +package io.quarkus.opentelemetry.runtime.tracing.cdi; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.api.annotation.support.ParameterAttributeNamesExtractor; +import io.quarkus.arc.ArcInvocationContext; + +/** + * Will capture the arguments annotated with {@link SpanAttribute} on methods annotated with {@link AddingSpanAttributes}. + * Will not start a Span if one is not already started. + */ +@SuppressWarnings("CdiInterceptorInspection") +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE) +public class AddingSpanAttributesInterceptor { + + private final WithSpanParameterAttributeNamesExtractor extractor; + + public AddingSpanAttributesInterceptor() { + extractor = new WithSpanParameterAttributeNamesExtractor(); + } + + @AroundInvoke + public Object span(final ArcInvocationContext invocationContext) throws Exception { + String[] extractedParameterNames = extractor.extract(invocationContext.getMethod(), + invocationContext.getMethod().getParameters()); + Object[] parameterValues = invocationContext.getParameters(); + + Span span = Span.current(); + if (span.isRecording()) { + try (Scope scope = span.makeCurrent()) { + for (int i = 0; i < extractedParameterNames.length; i++) { + if (extractedParameterNames[i] == null || parameterValues[i] == null) { + continue; + } + span.setAttribute(extractedParameterNames[i], parameterValues[i].toString()); + } + } + } + return invocationContext.proceed(); + } + + private static final class WithSpanParameterAttributeNamesExtractor implements ParameterAttributeNamesExtractor { + @Override + public String[] extract(final Method method, final Parameter[] parameters) { + String[] attributeNames = new String[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + attributeNames[i] = attributeName(parameters[i]); + } + return attributeNames; + } + + private static String attributeName(Parameter parameter) { + String value; + SpanAttribute spanAttribute = parameter.getDeclaredAnnotation(SpanAttribute.class); + if (spanAttribute == null) { + // Needed because SpanAttribute cannot be transformed + io.opentelemetry.extension.annotations.SpanAttribute legacySpanAttribute = parameter.getDeclaredAnnotation( + io.opentelemetry.extension.annotations.SpanAttribute.class); + if (legacySpanAttribute == null) { + return null; + } else { + value = legacySpanAttribute.value(); + } + } else { + value = spanAttribute.value(); + } + + if (!value.isEmpty()) { + return value; + } else if (parameter.isNamePresent()) { + return parameter.getName(); + } else { + return null; + } + } + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 8222854e51be20..746699977266af 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -141,6 +141,7 @@ oidc-client-reactive-filter oidc-token-propagation oidc-token-propagation-reactive + oidc-db-token-state-manager keycloak-authorization keycloak-admin-client-common keycloak-admin-client diff --git a/extensions/reactive-db2-client/runtime/pom.xml b/extensions/reactive-db2-client/runtime/pom.xml index 8ea8d89f8db39c..254cda26cb106e 100644 --- a/extensions/reactive-db2-client/runtime/pom.xml +++ b/extensions/reactive-db2-client/runtime/pom.xml @@ -49,6 +49,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.reactive-db2-client + + maven-compiler-plugin diff --git a/extensions/reactive-mssql-client/runtime/pom.xml b/extensions/reactive-mssql-client/runtime/pom.xml index 8fa6801bdb7fa0..aa9ba57b9f1449 100644 --- a/extensions/reactive-mssql-client/runtime/pom.xml +++ b/extensions/reactive-mssql-client/runtime/pom.xml @@ -53,6 +53,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.reactive-mssql-client + + maven-compiler-plugin diff --git a/extensions/reactive-mysql-client/runtime/pom.xml b/extensions/reactive-mysql-client/runtime/pom.xml index f8229fb01f6491..f98ea175f5550a 100644 --- a/extensions/reactive-mysql-client/runtime/pom.xml +++ b/extensions/reactive-mysql-client/runtime/pom.xml @@ -69,6 +69,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.reactive-mysql-client + + maven-compiler-plugin diff --git a/extensions/reactive-oracle-client/runtime/pom.xml b/extensions/reactive-oracle-client/runtime/pom.xml index 2a07cfd2169f3d..842a846f3c9b11 100644 --- a/extensions/reactive-oracle-client/runtime/pom.xml +++ b/extensions/reactive-oracle-client/runtime/pom.xml @@ -57,6 +57,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.reactive-oracle-client + + maven-compiler-plugin diff --git a/extensions/reactive-pg-client/runtime/pom.xml b/extensions/reactive-pg-client/runtime/pom.xml index 72d18586745532..21ea768d8973b8 100644 --- a/extensions/reactive-pg-client/runtime/pom.xml +++ b/extensions/reactive-pg-client/runtime/pom.xml @@ -57,6 +57,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.reactive-pg-client + + maven-compiler-plugin diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 1fc71b3db0415f..079419c1a867da 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -48,7 +48,7 @@ 2.0.1 1.7.0 - 3.1.3 + 3.1.5 3.5.3.Final 2.2.0 1.6.Final diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java index f98d4bda71cd49..3174b2976989fd 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java @@ -388,7 +388,8 @@ public TypeAndQualifiers(Type type, Set qualifiers) { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((qualifiers == null) ? 0 : qualifiers.hashCode()); + // We cannot use AnnotationInstance#hashCode() as it includes the AnnotationTarget + result = prime * result + annotationSetHashCode(qualifiers); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @@ -409,8 +410,8 @@ public boolean equals(Object obj) { if (other.qualifiers != null) { return false; } - } else if (!qualifiersAreEqual(qualifiers, other.qualifiers)) { - // We cannot use AnnotationInstance#equals() as it requires the exact same annotationTarget instance + } else if (!annotationSetEquals(qualifiers, other.qualifiers)) { + // We cannot use AnnotationInstance#equals() as it requires the exact same AnnotationTarget instance return false; } if (type == null) { @@ -423,16 +424,16 @@ public boolean equals(Object obj) { return true; } - private boolean qualifiersAreEqual(Set q1, Set q2) { - if (q1 == q2) { + private static boolean annotationSetEquals(Set s1, Set s2) { + if (s1 == s2) { return true; } - if (q1.size() != q2.size()) { + if (s1.size() != s2.size()) { return false; } - for (AnnotationInstance a1 : q1) { - for (AnnotationInstance a2 : q2) { - if (!annotationsAreEqual(a1, a2)) { + for (AnnotationInstance a1 : s1) { + for (AnnotationInstance a2 : s2) { + if (!annotationEquals(a1, a2)) { return false; } } @@ -440,13 +441,27 @@ private boolean qualifiersAreEqual(Set q1, Set s) { + int result = 1; + for (AnnotationInstance a : s) { + result = 31 * result + annotationHashCode(a); + } + return result; + } + + private static int annotationHashCode(AnnotationInstance a) { + int result = a.name().hashCode(); + result = 31 * result + a.values().hashCode(); + return result; + } + } } diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 1236a615edb48b..daf37730e53a9e 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -40,7 +40,7 @@ 3.11.0 3.2.1 3.1.2 - 3.1.3 + 3.1.5 3.24.2 @@ -63,7 +63,7 @@ 1.16.0 2.13.0 3.12.0 - 32.1.1-jre + 32.1.2-jre 1.0.1 2.8 1.2.6 diff --git a/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index e7a5826dae2e6a..0bded1328c8b00 100644 --- a/independent-projects/enforcer-rules/pom.xml +++ b/independent-projects/enforcer-rules/pom.xml @@ -109,7 +109,7 @@ org.apache.groovy groovy - 4.0.13 + 4.0.15 diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml index f3cac0d65d5bcb..0f5a5618ac3f18 100644 --- a/independent-projects/junit5-virtual-threads/pom.xml +++ b/independent-projects/junit5-virtual-threads/pom.xml @@ -44,7 +44,7 @@ 3.11.0 3.2.1 3.1.2 - 3.1.3 + 3.1.5 2.23.0 1.9.0 @@ -295,4 +295,4 @@ - \ No newline at end of file + diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index 551ebe66ac214c..af929866376141 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -43,7 +43,7 @@ 11 5.10.0 3.24.2 - 3.1.3 + 3.1.5 1.7.0 3.5.3.Final 3.11.0 diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 00ce1bdcaf0367..3a05b82f63ae95 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -41,14 +41,14 @@ 2.1.2 3.1.0 4.0.1 - 1.1.2 + 1.1.4 UTF-8 11 11 11 4.0.1 - 3.1.3 + 3.1.5 1.12.12 5.10.0 3.9.3 diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 00bd02a7bc9181..10ed0138fb11c4 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -62,7 +62,7 @@ 3.1.2 ${project.version} 25 - 3.1.3 + 3.1.5 2.0.2 4.2.0 diff --git a/integration-tests/smallrye-jwt-oidc-webapp/pom.xml b/integration-tests/smallrye-jwt-oidc-webapp/pom.xml index d8e3343d5ab5bc..e2494bd6d5f974 100644 --- a/integration-tests/smallrye-jwt-oidc-webapp/pom.xml +++ b/integration-tests/smallrye-jwt-oidc-webapp/pom.xml @@ -23,6 +23,14 @@ org.keycloak keycloak-adapter-core + + io.quarkus + quarkus-reactive-oracle-client + + + io.quarkus + quarkus-oidc-db-token-state-manager + org.keycloak keycloak-core @@ -99,6 +107,32 @@ + + io.quarkus + quarkus-oidc-db-token-state-manager-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-reactive-oracle-client-deployment + ${project.version} + pom + test + + + * + * + + + net.sourceforge.htmlunit @@ -193,6 +227,7 @@ http://localhost:8180/auth + vertx-reactive:oracle:thin:@localhost:1521/FREEPDB1 @@ -226,6 +261,31 @@ + + ${oracle.image} + + + 1521:1521 + + + oidc_db_token_state_manager_test + + + Oracle Database: + default + red + + + + + + + yes + + + + + true diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/PublicResource.java b/integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/PublicResource.java new file mode 100644 index 00000000000000..6ec8e109b465f1 --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/PublicResource.java @@ -0,0 +1,26 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.vertx.mutiny.oracleclient.OraclePool; + +@Path("/public") +public class PublicResource { + + @Inject + OraclePool pool; + + @Path("/token-state-count") + @GET + public int tokenStateCount() { + return pool + .query("SELECT COUNT(*) FROM oidc_db_token_state_manager") + .execute() + .map(rs -> rs.iterator().next().getInteger(0)) + .await() + .indefinitely(); + } + +} diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties b/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties index 117d7a88842f94..f9644529f38aac 100644 --- a/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties @@ -10,4 +10,7 @@ quarkus.oidc.application-type=web-app quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret - +quarkus.datasource.db-kind=oracle +quarkus.datasource.username=SYSTEM +quarkus.datasource.password=oidc_db_token_state_manager_test +quarkus.datasource.reactive.url=${reactive-oracledb.url} diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java index 54fb9c01b92dbf..d2e4089bb9f478 100644 --- a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; @@ -64,6 +65,7 @@ public void testNoToken() { @Test public void testGetUserNameWithCodeFlow() throws Exception { try (final WebClient webClient = createWebClient()) { + assertTokenStateCount(0); HtmlPage page = webClient.getPage("http://localhost:8081/protected"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -77,6 +79,7 @@ public void testGetUserNameWithCodeFlow() throws Exception { assertEquals("alice", page.getBody().asNormalizedText()); webClient.getCookieManager().clearCookies(); + assertTokenStateCount(1); } } @@ -85,4 +88,12 @@ private WebClient createWebClient() { webClient.setCssErrorHandler(new SilentCssErrorHandler()); return webClient; } + + private static void assertTokenStateCount(Integer expectedNumOfTokens) { + RestAssured + .get("/public/token-state-count") + .then() + .statusCode(200) + .body(Matchers.is(expectedNumOfTokens.toString())); + } } diff --git a/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/config/UnknownConfigFilesTest.java b/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/config/UnknownConfigFilesTest.java new file mode 100644 index 00000000000000..1537939af1b1eb --- /dev/null +++ b/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/config/UnknownConfigFilesTest.java @@ -0,0 +1,38 @@ +package io.quarkus.config; + +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.stream.Collectors; + +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class UnknownConfigFilesTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(EmptyAsset.INSTANCE, "application.properties") + .addAsResource(EmptyAsset.INSTANCE, "application-prod.properties") + .addAsResource(EmptyAsset.INSTANCE, "application.yaml")) + .setLogRecordPredicate(record -> record.getLevel().intValue() >= Level.WARNING.intValue()) + .assertLogRecords(logRecords -> { + List unknownConfigFiles = logRecords.stream() + .filter(l -> l.getMessage().startsWith("Unrecognized configuration file")) + .collect(Collectors.toList()); + + assertEquals(1, unknownConfigFiles.size()); + assertTrue(unknownConfigFiles.get(0).getParameters()[0].toString().contains("application.yaml")); + }); + + @Test + void unknownConfigFiles() { + + } +}