diff --git a/.github/native-tests.json b/.github/native-tests.json index 11d2eca78c18a..75e7d622386da 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -133,9 +133,9 @@ "os-name": "ubuntu-latest" }, { - "category": "Windows - RESTEasy Jackson", - "timeout": 25, - "test-modules": "resteasy-jackson", + "category": "Windows support", + "timeout": 50, + "test-modules": "resteasy-jackson, qute", "os-name": "windows-latest" }, { diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 528def08a61fa..56338496e63ef 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -189,7 +189,7 @@ 5.8.0 4.13.0 2.0.3.Final - 23.0.7 + 24.0.4 1.15.1 3.42.0 2.24.0 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index c59ffce05a4dc..3f1b15583e888 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -106,7 +106,7 @@ - 23.0.7 + 24.0.4 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java index feda25cbade14..dccb19214baec 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import io.quarkus.deployment.annotations.BuildProducer; @@ -46,8 +45,8 @@ public void set(List setCPItems, } } - String classPathValue = Stream.concat(parentFirst.stream(), regular.stream()).map(p -> p.toAbsolutePath().toString()) - .collect(Collectors.joining(":")); - recorder.set(classPathValue); + List allJarPaths = Stream.concat(parentFirst.stream(), regular.stream()).map(p -> p.toAbsolutePath().toString()) + .toList(); + recorder.set(allJarPaths); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java index fdca4fcb0cb65..c5f17f4b8d0f6 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java @@ -1,11 +1,13 @@ package io.quarkus.runtime; +import java.util.List; + import io.quarkus.runtime.annotations.Recorder; @Recorder public class ClassPathSystemPropertyRecorder { - public void set(String value) { - System.setProperty("java.class.path", value); + public void set(List allJarPaths) { + System.setProperty("java.class.path", String.join(":", allJarPaths)); } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index 9dc4a552aad8c..d8cb0a39347e2 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -745,15 +744,15 @@ private void executeGoal(PluginExec pluginExec, String goal, Map private List readAnnotationProcessors(Xpp3Dom pluginConfig) { if (pluginConfig == null) { - return Collections.emptyList(); + return List.of(); } Xpp3Dom annotationProcessors = pluginConfig.getChild("annotationProcessors"); if (annotationProcessors == null) { - return Collections.emptyList(); + return List.of(); } Xpp3Dom[] processors = annotationProcessors.getChildren("annotationProcessor"); if (processors.length == 0) { - return Collections.emptyList(); + return List.of(); } List ret = new ArrayList<>(processors.length); for (Xpp3Dom processor : processors) { @@ -764,21 +763,18 @@ private List readAnnotationProcessors(Xpp3Dom pluginConfig) { private Set readAnnotationProcessorPaths(Xpp3Dom pluginConfig) throws MojoExecutionException { if (pluginConfig == null) { - return Collections.emptySet(); + return Set.of(); } Xpp3Dom annotationProcessorPaths = pluginConfig.getChild("annotationProcessorPaths"); if (annotationProcessorPaths == null) { - return Collections.emptySet(); + return Set.of(); } + var versionConstraints = getAnnotationProcessorPathsDepMgmt(pluginConfig); Xpp3Dom[] paths = annotationProcessorPaths.getChildren("path"); Set elements = new LinkedHashSet<>(); try { List dependencies = convertToDependencies(paths); - // NOTE: The Maven Compiler Plugin also supports a flag (disabled by default) for applying managed dependencies to - // the dependencies of the APT plugins (not them directly), which we don't support yet here - // you can find the implementation at https://github.com/apache/maven-compiler-plugin/pull/180/files#diff-d4bac42d8f4c68d397ddbaa05c1cbbed7984ef6dc0bb9ea60739df78997e99eeR1610 - // when/if we need it - CollectRequest collectRequest = new CollectRequest(dependencies, Collections.emptyList(), + CollectRequest collectRequest = new CollectRequest(dependencies, versionConstraints, project.getRemoteProjectRepositories()); DependencyRequest dependencyRequest = new DependencyRequest(); dependencyRequest.setCollectRequest(collectRequest); @@ -795,6 +791,18 @@ private Set readAnnotationProcessorPaths(Xpp3Dom pluginConfig) throws Mojo } } + private List getAnnotationProcessorPathsDepMgmt(Xpp3Dom pluginConfig) { + final Xpp3Dom useDepMgmt = pluginConfig.getChild("annotationProcessorPathsUseDepMgmt"); + if (useDepMgmt == null || !Boolean.parseBoolean(useDepMgmt.getValue())) { + return List.of(); + } + var dm = project.getDependencyManagement(); + if (dm == null) { + return List.of(); + } + return getProjectAetherDependencyManagement(); + } + private List convertToDependencies(Xpp3Dom[] paths) throws MojoExecutionException { List dependencies = new ArrayList<>(); for (Xpp3Dom path : paths) { @@ -847,7 +855,7 @@ private String toNullIfEmpty(String value) { private List getProjectManagedDependencies() { DependencyManagement dependencyManagement = project.getDependencyManagement(); if (dependencyManagement == null || dependencyManagement.getDependencies() == null) { - return Collections.emptyList(); + return List.of(); } return dependencyManagement.getDependencies(); } @@ -863,7 +871,7 @@ private String getValue(Xpp3Dom path, String element, String defaultValue) { private Set convertToAetherExclusions(Xpp3Dom exclusions) { if (exclusions == null) { - return Collections.emptySet(); + return Set.of(); } Set aetherExclusions = new HashSet<>(); for (Xpp3Dom exclusion : exclusions.getChildren("exclusion")) { @@ -1487,21 +1495,6 @@ private void addQuarkusDevModeDeps(MavenDevModeLauncher.Builder builder, Applica throw new MojoExecutionException("Classpath resource " + pomPropsPath + " is missing version"); } - final List managed = new ArrayList<>( - project.getDependencyManagement().getDependencies().size()); - project.getDependencyManagement().getDependencies().forEach(d -> { - final List exclusions; - if (!d.getExclusions().isEmpty()) { - exclusions = new ArrayList<>(d.getExclusions().size()); - d.getExclusions().forEach(e -> exclusions.add(new Exclusion(e.getGroupId(), e.getArtifactId(), "*", "*"))); - } else { - exclusions = List.of(); - } - managed.add(new org.eclipse.aether.graph.Dependency( - new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion()), - d.getScope(), d.isOptional(), exclusions)); - }); - final DefaultArtifact devModeJar = new DefaultArtifact(devModeGroupId, devModeArtifactId, ArtifactCoords.TYPE_JAR, devModeVersion); final DependencyResult cpRes = repoSystem.resolveDependencies(repoSession, @@ -1511,7 +1504,7 @@ private void addQuarkusDevModeDeps(MavenDevModeLauncher.Builder builder, Applica // it doesn't matter what the root artifact is, it's an alias .setRootArtifact(new DefaultArtifact(IO_QUARKUS, "quarkus-devmode-alias", ArtifactCoords.TYPE_JAR, "1.0")) - .setManagedDependencies(managed) + .setManagedDependencies(getProjectAetherDependencyManagement()) .setDependencies(List.of( new org.eclipse.aether.graph.Dependency(devModeJar, JavaScopes.RUNTIME), new org.eclipse.aether.graph.Dependency(new DefaultArtifact( @@ -1537,6 +1530,24 @@ private void addQuarkusDevModeDeps(MavenDevModeLauncher.Builder builder, Applica } } + private List getProjectAetherDependencyManagement() { + final List managed = new ArrayList<>( + project.getDependencyManagement().getDependencies().size()); + project.getDependencyManagement().getDependencies().forEach(d -> { + final List exclusions; + if (!d.getExclusions().isEmpty()) { + exclusions = new ArrayList<>(d.getExclusions().size()); + d.getExclusions().forEach(e -> exclusions.add(new Exclusion(e.getGroupId(), e.getArtifactId(), "*", "*"))); + } else { + exclusions = List.of(); + } + managed.add(new org.eclipse.aether.graph.Dependency( + new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion()), + d.getScope(), d.isOptional(), exclusions)); + }); + return managed; + } + private void setKotlinSpecificFlags(MavenDevModeLauncher.Builder builder) { Plugin kotlinMavenPlugin = null; for (Plugin plugin : project.getBuildPlugins()) { diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index e2c3beabca857..5b323b5615d43 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -507,6 +507,75 @@ public class MyProducer { ---- ==== +[[datasource-multiple-single-transaction]] +=== Use multiple datasources in a single transaction + +By default, XA support on datasources is disabled, +and thus a transaction may include at most one datasource. +Attempting to access multiple non-XA datasources in the same transaction +is unsafe and will result in a warning. + +In Quarkus 3.10+, this will result in an error by default. + +To safely allow using multiple JDBC datasources in the same transaction: + +. Make sure your JDBC driver supports XA. +All <>, +but <> might not. +. Make sure your database server is configured to enable XA. +. Enable XA support explicitly for each relevant datasource by setting +<> to `xa`. + +Using XA, a rollback in one datasource will trigger a rollback in every other datasource enrolled in the transaction. + +[NOTE] +==== +XA transactions on reactive datasources are not supported at the moment. +==== + +[NOTE] +==== +If your transaction involves other, non-datasource resources, +keep in mind *those* resources might not support XA transactions, +or might require additional configuration. +==== + +If XA cannot be enabled for one of your datasources: + +* Be aware that enabling XA for all datasources _except one_ (and only one) is still supported +through https://www.narayana.io/docs/project/index.html#d5e857[Last Resource Commit Optimization (LRCO)]. +* If you do not need a rollback for one datasource to trigger a rollback for other datasources, +consider splitting your code into multiple transactions. +To that end, use xref:transaction.adoc#programmatic-approach[`QuarkusTransaction.requiringNew()`]/xref:transaction.adoc#declarative-approach[`@Transactional(REQUIRES_NEW)`] (preferably) +or xref:transaction.adoc#legacy-api-approach[`UserTransaction`] (for more complex use cases). + +[CAUTION] +==== +If you want to make sure it is impossible to access multiple non-XA datasources in the same transaction, +you can set `quarkus.transaction-manager.unsafe-multiple-last-resources` to `fail`. +This value is the default for Quarkus 3.10+. + +With any value different from `fail`, a transaction rollback +could possibly be applied to only some of the non-XA datasources, +with other non-XA datasources having already committed their changes, +leaving your overall system in an inconsistent state. + +By default in Quarkus 3.8, for compatibility reasons, +you get warned once for the first occurrence (`warn-first`). +You may fully allow unsafe transaction handling across multiple non-XA datasources +by setting `quarkus.transaction-manager.unsafe-multiple-last-resources` to `allow`. + +Alternatively, you can allow the same unsafe behavior +but logging a warning on *each* offending transaction by setting the property to `warn-each`. + +We do not recommend using this configuration property, +and we plan to remove it in the future, +so you should plan fixing your application accordingly. +If you think your use case of this feature is valid and this option should be kept around, +open an issue in the https://github.com/quarkusio/quarkus/issues/new?assignees=&labels=kind%2Fenhancement&projects=&template=feature_request.yml[Quarkus tracker] +explaining why. +==== + == Datasource integrations === Datasource health check diff --git a/docs/src/main/asciidoc/hibernate-reactive.adoc b/docs/src/main/asciidoc/hibernate-reactive.adoc index e63ce57c8d5f5..f685e4970679a 100644 --- a/docs/src/main/asciidoc/hibernate-reactive.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive.adoc @@ -191,7 +191,7 @@ and will have it use the default datasource. The configuration properties listed here allow you to override such defaults, and customize and tune various aspects. Hibernate Reactive uses the same properties you would use for Hibernate ORM. You will notice that some properties -contain `jdbc` in the name but there is not JDBC in Hibernate Reactive, these are simply legacy property names. +contain `jdbc` in the name but there is no JDBC in Hibernate Reactive, these are simply legacy property names. include::{generated-dir}/config/quarkus-hibernate-orm.adoc[opts=optional, leveloffset=+2] diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 7dfe2d21f92c4..4ee212c1a25d4 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1340,8 +1340,12 @@ A very simple Jakarta REST Resource that uses `Person` could be: ---- package org.acme.rest; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; @Path("person") public class Person { @@ -1351,8 +1355,25 @@ public class Person { public Person getPerson(Long id) { return new Person(id, "foo", "bar", "Brick Lane"); } + + @Produces(APPLICATION_JSON) <1> + @Path("/friend/{id}") + @GET + public Response getPersonFriend(Long id) { + var person = new Person(id, "foo", "bar", "Brick Lane"); + return Response.ok(person).build(); + } } ---- +<1> The `@SecureField` annotation is only effective when Quarkus recognizes that produced content type is the 'application/json' type. + +WARNING: Currently you cannot use the `@SecureField` annotation to secure your data returned from resource methods returning the `io.smallrye.mutiny.Multi` reactive type. + +[IMPORTANT] +==== +All resource methods returning data secured with the `@SecureField` annotation should be tested. +Please make sure data are secured as you intended. +==== Assuming security has been set up for the application (see our xref:security-overview.adoc[guide] for more details), when a user with the `admin` role performs an HTTP GET on `/person/1` they will receive: 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 fd3d37c2468ab..32ebe11900fc0 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -258,7 +258,7 @@ For more information, see xref:security-oidc-bearer-token-authentication.adoc#in [[keycloak-initialization]] === Keycloak initialization -The `quay.io/keycloak/keycloak:23.0.7` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +The `quay.io/keycloak/keycloak:24.0.4` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. `quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. Be aware that a Quarkus-based Keycloak distribution is only available starting from Keycloak `20.0.0`. diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index 171aaed2e66c6..a232d7c681eef 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -38,9 +38,9 @@ if [ -z $TARGET_DIR ]; then GIT_OPTIONS="--depth=1" fi if [ -n "${RELEASE_GITHUB_TOKEN}" ]; then - git clone -b develop --single-branch $GIT_OPTIONS https://github.com/quarkusio/quarkusio.github.io.git ${TARGET_DIR} + git clone --single-branch $GIT_OPTIONS https://github.com/quarkusio/quarkusio.github.io.git ${TARGET_DIR} else - git clone -b develop --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} + git clone --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} fi fi @@ -143,7 +143,7 @@ then cd target/web-site git add -A git commit -m "Sync web site with Quarkus documentation" - git push origin develop + git push origin main echo "Web Site updated - wait for CI build" else echo " diff --git a/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/BaseFunction.java b/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/BaseFunction.java index 0f409fc01f076..9a3341f335e2a 100644 --- a/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/BaseFunction.java +++ b/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/BaseFunction.java @@ -3,6 +3,7 @@ import java.io.ByteArrayOutputStream; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -62,7 +63,7 @@ protected HttpResponseMessage nettyDispatch(HttpRequestMessage> HttpContent requestContent = LastHttpContent.EMPTY_LAST_CONTENT; if (request.getBody().isPresent()) { - ByteBuf body = Unpooled.wrappedBuffer(request.getBody().get().getBytes()); + ByteBuf body = Unpooled.wrappedBuffer(request.getBody().get().getBytes(StandardCharsets.UTF_8)); requestContent = new DefaultLastHttpContent(body); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index bbb55c3e1ec94..d09507d72b2de 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -16,7 +16,7 @@ void testImageWithJava17() { Path path = getPath("openjdk-17-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } @@ -26,7 +26,7 @@ void testImageWithJava21() { Path path = getPath("openjdk-21-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19"); assertThat(v.getJavaVersion()).isEqualTo(21); }); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime index 9bc56f98c9d33..a06add4a4733e 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18 +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime index 8d11343e7b78e..0a470b183b8da 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime @@ -1,5 +1,5 @@ -# Use Java 17 base image -FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18 +# Use Java 21 base image +FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java index d9988bc6b79c5..1727d065f9f4b 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java @@ -16,9 +16,9 @@ public class ContainerImageJibConfig { /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java index c8ad868835b6d..df20d64f6c596 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java @@ -15,8 +15,8 @@ @ConfigRoot(name = "openshift", phase = ConfigPhase.BUILD_TIME) public class ContainerImageOpenshiftConfig { - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.18"; - public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.18"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.19"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.19"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -47,9 +47,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion * The value of this property is used to create an ImageStream for the builder image used in the Openshift build. * When it references images already available in the internal Openshift registry, the corresponding streams are used * instead. - * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.18} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.19} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.19} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java index defde810dc8a7..675519cd28f9a 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java @@ -12,8 +12,8 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class S2iConfig { - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.18"; - public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.18"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.19"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.19"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.18} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.19} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.19} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/ClassHierarchyTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/ClassHierarchyTest.java new file mode 100644 index 0000000000000..a9dd211d283b9 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/ClassHierarchyTest.java @@ -0,0 +1,63 @@ +package io.quarkus.hibernate.validator.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.AbstractCollection; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.Validator; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.ByteArrayAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType; + +public class ClassHierarchyTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> { + JavaArchive javaArchive = ShrinkWrap.create(JavaArchive.class) + .addClass(Dto.class); + // Create an inner class with an incomplete hierarchy + try (DynamicType.Unloaded superClass = new ByteBuddy() + .subclass(Object.class) + .name("SuperClass") + .make(); + DynamicType.Unloaded outerClass = new ByteBuddy() + .subclass(superClass.getTypeDescription()) + .name("OuterClass") + .make(); + DynamicType.Unloaded innerClass = new ByteBuddy() + .subclass(AbstractCollection.class) + .innerTypeOf(outerClass.getTypeDescription()) + .name("InnerClass") + .make(); + DynamicType.Loaded innerLoad = innerClass.load(Thread.currentThread().getContextClassLoader())) { + javaArchive.add(new ByteArrayAsset(innerLoad.getBytes()), "InnerClass.class"); + } + return javaArchive; + }); + + @Inject + Validator validator; + + @Test + public void doNotFailWhenLoadingIncompleteClassHierarchy() { + assertThat(validator).isNotNull(); + } + + @Valid + public static class Dto { + String name; + + // InnerClass is a subclass with an incomplete hierarchy + @Valid + AbstractCollection items; + } +} diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java index 88b808fd14142..40429cc069436 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; @@ -76,6 +77,9 @@ public void created(BeanContainer container) { configuration.localeResolver(localeResolver); } + // Filter out classes with incomplete hierarchy + filterIncompleteClasses(classesToBeValidated); + configuration.builtinConstraints(detectedBuiltinConstraints) .initializeBeanMetaData(classesToBeValidated) // Locales, Locale ROOT means all locales in this setting. @@ -188,6 +192,22 @@ public void run() { } }); } + + /** + * Filter out classes with incomplete hierarchy + */ + private void filterIncompleteClasses(Set> classesToBeValidated) { + Iterator> iterator = classesToBeValidated.iterator(); + while (iterator.hasNext()) { + Class clazz = iterator.next(); + try { + // This should trigger a NoClassDefFoundError if the class has an incomplete hierarchy + clazz.getCanonicalName(); + } catch (NoClassDefFoundError e) { + iterator.remove(); + } + } + } }; return beanContainerListener; diff --git a/extensions/jdbc/jdbc-h2/runtime/pom.xml b/extensions/jdbc/jdbc-h2/runtime/pom.xml index fb184e03fb02e..6953ca4c327eb 100644 --- a/extensions/jdbc/jdbc-h2/runtime/pom.xml +++ b/extensions/jdbc/jdbc-h2/runtime/pom.xml @@ -52,6 +52,9 @@ com.h2database:h2 + + io.quarkus.jdbc.h2 + diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java index 9bfd954d11e47..8167b900a952e 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java @@ -2,6 +2,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; /** @@ -14,7 +15,8 @@ public final class OracleNativeImage { * by reflection, as commonly expected. */ @BuildStep - void reflection(BuildProducer reflectiveClass) { + void reflection(BuildProducer reflectiveClass, + BuildProducer additionalIndexedClasses) { //Not strictly necessary when using Agroal, as it also registers //any JDBC driver being configured explicitly through its configuration. //We register it for the sake of people not using Agroal. @@ -23,6 +25,10 @@ void reflection(BuildProducer reflectiveClass) { final String driverName = "oracle.jdbc.driver.OracleDriver"; reflectiveClass.produce(ReflectiveClassBuildItem.builder(driverName).build()); + // This is needed when using XA and we use the `@RegisterForReflection` trick to make sure all nested classes are registered for reflection + additionalIndexedClasses + .produce(new AdditionalIndexedClassesBuildItem("io.quarkus.jdbc.oracle.runtime.graal.OracleReflections")); + // for ldap style jdbc urls. e.g. jdbc:oracle:thin:@ldap://oid:5000/mydb1,cn=OracleContext,dc=myco,dc=com // // Note that all JDK provided InitialContextFactory impls from the JDK registered via module descriptors diff --git a/extensions/jdbc/jdbc-oracle/runtime/src/main/java/io/quarkus/jdbc/oracle/runtime/graal/OracleReflections.java b/extensions/jdbc/jdbc-oracle/runtime/src/main/java/io/quarkus/jdbc/oracle/runtime/graal/OracleReflections.java new file mode 100644 index 0000000000000..f489dc605beea --- /dev/null +++ b/extensions/jdbc/jdbc-oracle/runtime/src/main/java/io/quarkus/jdbc/oracle/runtime/graal/OracleReflections.java @@ -0,0 +1,13 @@ +package io.quarkus.jdbc.oracle.runtime.graal; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * We don't use a build item here as we also need to register all the nested classes and there's no way to do it easily with the + * build item for now. + */ +@RegisterForReflection(targets = { oracle.jdbc.xa.OracleXADataSource.class, + oracle.jdbc.datasource.impl.OracleDataSource.class }) +public class OracleReflections { + +} diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java index 504bc66168f4e..ba0f407ea5dec 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java @@ -35,6 +35,9 @@ public Map createKafkaRuntimeConfig(Config config, ApplicationCo if (!propertyNameLowerCase.startsWith(CONFIG_PREFIX) || propertyNameLowerCase.startsWith(UI_CONFIG_PREFIX)) { continue; } + if (propertyNameLowerCase.length() <= CONFIG_PREFIX.length()) { + continue; + } // Replace _ by . - This is because Kafka properties tend to use . and env variables use _ for every special // character. So, replace _ with . String effectivePropertyName = propertyNameLowerCase.substring(CONFIG_PREFIX.length() + 1).toLowerCase() diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java index ab68ddd370514..c6ed9377024b0 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java @@ -16,6 +16,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,6 +37,7 @@ import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -101,6 +103,7 @@ void nativeImageConfiguration( LiquibaseBuildTimeConfig liquibaseBuildConfig, List jdbcDataSourceBuildItems, CombinedIndexBuildItem combinedIndex, + Capabilities capabilities, BuildProducer reflective, BuildProducer resource, BuildProducer services, @@ -209,7 +212,7 @@ void nativeImageConfiguration( // CommandStep implementations are needed consumeService(liquibase.command.CommandStep.class, (serviceClass, implementations) -> { var filteredImpls = implementations.stream() - .filter(not("liquibase.command.core.StartH2CommandStep"::equals)) + .filter(commandStepPredicate(capabilities)) .toArray(String[]::new); services.produce(new ServiceProviderBuildItem(serviceClass.getName(), filteredImpls)); reflective.produce(ReflectiveClassBuildItem.builder(filteredImpls).constructors().build()); @@ -247,6 +250,14 @@ void nativeImageConfiguration( resourceBundle.produce(new NativeImageResourceBundleBuildItem("liquibase/i18n/liquibase-core")); } + private static Predicate commandStepPredicate(Capabilities capabilities) { + if (capabilities.isPresent("io.quarkus.jdbc.h2")) { + return (s) -> true; + } else { + return not("liquibase.command.core.StartH2CommandStep"::equals); + } + } + private void consumeService(Class serviceClass, BiConsumer, Collection> consumer) { try { String service = "META-INF/services/" + serviceClass.getName(); diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/HttpCommonTags.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/HttpCommonTags.java index 656736a6d345c..7cab35f4f0446 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/HttpCommonTags.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/HttpCommonTags.java @@ -1,5 +1,7 @@ package io.quarkus.micrometer.runtime.binder; +import java.util.Objects; + import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.binder.http.Outcome; @@ -46,22 +48,18 @@ public static Tag outcome(int statusCode) { /** * Creates a {@code uri} tag based on the URI of the given {@code request}. - * Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} - * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN} + * Falling back to {@code REDIRECTION} for 3xx responses if there wasn't a matched path pattern, {@code NOT_FOUND} + * for 404 responses if there wasn't a matched path pattern, {@code root} for requests with no path info, and + * {@code UNKNOWN} * for all other requests. * * @param pathInfo request path + * @param initialPath initial path before request pattern matching took place. Pass in null if there is pattern matching + * done in the caller. * @param code status code of the response * @return the uri tag derived from the request */ - public static Tag uri(String pathInfo, int code) { - if (code > 0) { - if (code / 100 == 3) { - return URI_REDIRECTION; - } else if (code == 404) { - return URI_NOT_FOUND; - } - } + public static Tag uri(String pathInfo, String initialPath, int code) { if (pathInfo == null) { return URI_UNKNOWN; } @@ -69,7 +67,28 @@ public static Tag uri(String pathInfo, int code) { return URI_ROOT; } + if (code > 0) { + if (code / 100 == 3) { + if (isTemplatedPath(pathInfo, initialPath)) { + return Tag.of("uri", pathInfo); + } else { + return URI_REDIRECTION; + } + } else if (code == 404) { + if (isTemplatedPath(pathInfo, initialPath)) { + return Tag.of("uri", pathInfo); + } else { + return URI_NOT_FOUND; + } + } + } + // Use first segment of request path return Tag.of("uri", pathInfo); } + + private static boolean isTemplatedPath(String pathInfo, String initialPath) { + // only include the path info if it has been matched to a template (initialPath != pathInfo) to avoid a metrics explosion with lots of entries + return initialPath != null && !Objects.equals(initialPath, pathInfo); + } } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsFilter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsFilter.java index 004aa63e2d162..f1c6acb745eed 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsFilter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsFilter.java @@ -72,7 +72,7 @@ public void filter(final ClientRequestContext requestContext, final ClientRespon Timer.Builder builder = Timer.builder(httpMetricsConfig.getHttpClientRequestsName()) .tags(Tags.of( HttpCommonTags.method(requestContext.getMethod()), - HttpCommonTags.uri(requestPath, statusCode), + HttpCommonTags.uri(requestPath, requestContext.getUri().getPath(), statusCode), HttpCommonTags.outcome(statusCode), HttpCommonTags.status(statusCode), clientName(requestContext))); diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java index 8e3fc3474b8e1..5d346eee3428a 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java @@ -183,7 +183,7 @@ public static class RequestTracker extends RequestMetricInfo { this.tags = origin.and( Tag.of("address", address), HttpCommonTags.method(method), - HttpCommonTags.uri(path, -1)); + HttpCommonTags.uri(path, null, -1)); } void requestReset() { diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java index 6f22060edc250..23b605d546109 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java @@ -99,7 +99,7 @@ public HttpRequestMetric responsePushed(LongTaskTimer.Sample socketMetric, HttpM config.getServerIgnorePatterns()); if (path != null) { registry.counter(nameHttpServerPush, Tags.of( - HttpCommonTags.uri(path, response.statusCode()), + HttpCommonTags.uri(path, requestMetric.initialPath, response.statusCode()), VertxMetricsTags.method(method), VertxMetricsTags.outcome(response), HttpCommonTags.status(response.statusCode()))) @@ -153,7 +153,7 @@ public void requestReset(HttpRequestMetric requestMetric) { Timer.Builder builder = Timer.builder(nameHttpServerRequests) .tags(Tags.of( VertxMetricsTags.method(requestMetric.request().method()), - HttpCommonTags.uri(path, 0), + HttpCommonTags.uri(path, requestMetric.initialPath, 0), Outcome.CLIENT_ERROR.asTag(), HttpCommonTags.STATUS_RESET)); @@ -180,7 +180,7 @@ public void responseEnd(HttpRequestMetric requestMetric, HttpResponse response, Timer.Sample sample = requestMetric.getSample(); Tags allTags = Tags.of( VertxMetricsTags.method(requestMetric.request().method()), - HttpCommonTags.uri(path, response.statusCode()), + HttpCommonTags.uri(path, requestMetric.initialPath, response.statusCode()), VertxMetricsTags.outcome(response), HttpCommonTags.status(response.statusCode())); if (!httpServerMetricsTagsContributors.isEmpty()) { @@ -217,7 +217,7 @@ public LongTaskTimer.Sample connected(LongTaskTimer.Sample sample, HttpRequestMe config.getServerIgnorePatterns()); if (path != null) { return LongTaskTimer.builder(nameWebsocketConnections) - .tags(Tags.of(HttpCommonTags.uri(path, 0))) + .tags(Tags.of(HttpCommonTags.uri(path, requestMetric.initialPath, 0))) .register(registry) .start(); } diff --git a/extensions/micrometer/runtime/src/test/java/io/quarkus/micrometer/runtime/binder/HttpCommonTagsTest.java b/extensions/micrometer/runtime/src/test/java/io/quarkus/micrometer/runtime/binder/HttpCommonTagsTest.java index 2474a9f228c6e..e6bf10ee83dbc 100644 --- a/extensions/micrometer/runtime/src/test/java/io/quarkus/micrometer/runtime/binder/HttpCommonTagsTest.java +++ b/extensions/micrometer/runtime/src/test/java/io/quarkus/micrometer/runtime/binder/HttpCommonTagsTest.java @@ -21,17 +21,21 @@ public void testStatus() { @Test public void testUriRedirect() { - Assertions.assertEquals(HttpCommonTags.URI_REDIRECTION, HttpCommonTags.uri("/moved", 301)); - Assertions.assertEquals(HttpCommonTags.URI_REDIRECTION, HttpCommonTags.uri("/moved", 302)); - Assertions.assertEquals(HttpCommonTags.URI_REDIRECTION, HttpCommonTags.uri("/moved", 304)); + Assertions.assertEquals(HttpCommonTags.URI_REDIRECTION, HttpCommonTags.uri("/moved", null, 301)); + Assertions.assertEquals(HttpCommonTags.URI_REDIRECTION, HttpCommonTags.uri("/moved", null, 302)); + Assertions.assertEquals(HttpCommonTags.URI_REDIRECTION, HttpCommonTags.uri("/moved", null, 304)); + Assertions.assertEquals(Tag.of("uri", "/moved/{id}"), HttpCommonTags.uri("/moved/{id}", "/moved/111", 304)); + Assertions.assertEquals(HttpCommonTags.URI_ROOT, HttpCommonTags.uri("/", null, 304)); } @Test public void testUriDefaults() { - Assertions.assertEquals(HttpCommonTags.URI_ROOT, HttpCommonTags.uri("/", 200)); - Assertions.assertEquals(Tag.of("uri", "/known/ok"), HttpCommonTags.uri("/known/ok", 200)); - Assertions.assertEquals(HttpCommonTags.URI_NOT_FOUND, HttpCommonTags.uri("/invalid", 404)); - Assertions.assertEquals(Tag.of("uri", "/known/bad/request"), HttpCommonTags.uri("/known/bad/request", 400)); - Assertions.assertEquals(Tag.of("uri", "/known/server/error"), HttpCommonTags.uri("/known/server/error", 500)); + Assertions.assertEquals(HttpCommonTags.URI_ROOT, HttpCommonTags.uri("/", null, 200)); + Assertions.assertEquals(HttpCommonTags.URI_ROOT, HttpCommonTags.uri("/", null, 404)); + Assertions.assertEquals(Tag.of("uri", "/known/ok"), HttpCommonTags.uri("/known/ok", null, 200)); + Assertions.assertEquals(HttpCommonTags.URI_NOT_FOUND, HttpCommonTags.uri("/invalid", null, 404)); + Assertions.assertEquals(Tag.of("uri", "/invalid/{id}"), HttpCommonTags.uri("/invalid/{id}", "/invalid/111", 404)); + Assertions.assertEquals(Tag.of("uri", "/known/bad/request"), HttpCommonTags.uri("/known/bad/request", null, 400)); + Assertions.assertEquals(Tag.of("uri", "/known/server/error"), HttpCommonTags.uri("/known/server/error", null, 500)); } } diff --git a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java index 396279af870c0..c661f5ea866b6 100644 --- a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java +++ b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java @@ -47,6 +47,8 @@ import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; @@ -56,16 +58,21 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.gizmo.ClassCreator; import io.quarkus.narayana.jta.runtime.NarayanaJtaProducers; import io.quarkus.narayana.jta.runtime.NarayanaJtaRecorder; +import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig; +import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode; import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration; import io.quarkus.narayana.jta.runtime.context.TransactionContext; +import io.quarkus.narayana.jta.runtime.graal.DisableLoggingFeature; import io.quarkus.narayana.jta.runtime.interceptor.TestTransactionInterceptor; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorMandatory; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNever; @@ -93,7 +100,11 @@ public void build(NarayanaJtaRecorder recorder, BuildProducer reflectiveClass, BuildProducer runtimeInit, BuildProducer feature, - TransactionManagerConfiguration transactions, ShutdownContextBuildItem shutdownContextBuildItem) { + BuildProducer logCleanupFilters, + BuildProducer nativeImageFeatures, + TransactionManagerConfiguration transactions, TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + ShutdownContextBuildItem shutdownContextBuildItem, + Capabilities capabilities) { recorder.handleShutdown(shutdownContextBuildItem, transactions); feature.produce(new FeatureBuildItem(Feature.NARAYANA_JTA)); additionalBeans.produce(new AdditionalBeanBuildItem(NarayanaJtaProducers.class)); @@ -137,6 +148,12 @@ public void build(NarayanaJtaRecorder recorder, builder.addBeanClass(TransactionalInterceptorNotSupported.class); additionalBeans.produce(builder.build()); + transactionManagerBuildTimeConfig.unsafeMultipleLastResources.ifPresent(mode -> { + if (!mode.equals(UnsafeMultipleLastResourcesMode.FAIL)) { + recorder.logUnsafeMultipleLastResourcesOnStartup(mode); + } + }); + //we want to force Arjuna to init at static init time Properties defaultProperties = PropertiesFactory.getDefaultProperties(); //we don't want to store the system properties here @@ -144,14 +161,28 @@ public void build(NarayanaJtaRecorder recorder, for (Object i : System.getProperties().keySet()) { defaultProperties.remove(i); } + recorder.setDefaultProperties(defaultProperties); // This must be done before setNodeName as the code in setNodeName will create a TSM based on the value of this property recorder.disableTransactionStatusManager(); + allowUnsafeMultipleLastResources(recorder, transactionManagerBuildTimeConfig, capabilities, logCleanupFilters, + nativeImageFeatures); recorder.setNodeName(transactions); recorder.setDefaultTimeout(transactions); recorder.setConfig(transactions); } + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + public void nativeImageFeature(TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + BuildProducer nativeImageFeatures) { + switch (transactionManagerBuildTimeConfig.unsafeMultipleLastResources + .orElse(UnsafeMultipleLastResourcesMode.DEFAULT)) { + case ALLOW, WARN_FIRST, WARN_EACH -> { + nativeImageFeatures.produce(new NativeImageFeatureBuildItem(DisableLoggingFeature.class)); + } + } + } + @BuildStep @Record(RUNTIME_INIT) @Consume(NarayanaInitBuildItem.class) @@ -211,4 +242,35 @@ void unremovableBean(BuildProducer unremovableBeans) { void logCleanupFilters(BuildProducer logCleanupFilters) { logCleanupFilters.produce(new LogCleanupFilterBuildItem("com.arjuna.ats.jbossatx", "ARJUNA032010:", "ARJUNA032013:")); } + + private void allowUnsafeMultipleLastResources(NarayanaJtaRecorder recorder, + TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + Capabilities capabilities, BuildProducer logCleanupFilters, + BuildProducer nativeImageFeatures) { + switch (transactionManagerBuildTimeConfig.unsafeMultipleLastResources + .orElse(UnsafeMultipleLastResourcesMode.DEFAULT)) { + case ALLOW -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), true); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012141", "ARJUNA012142")); + } + case WARN_FIRST -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), true); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + // but we still want Narayana to produce a warning on the first offending transaction + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012142")); + } + case WARN_EACH -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), false); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + // but we still want Narayana to produce one warning per offending transaction + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012142")); + } + case FAIL -> { // No need to do anything, this is the default behavior of Narayana + } + } + } } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java index 83d9f1b865cfa..156dbd6d1c865 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java @@ -29,6 +29,7 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.util.StringUtil; @Recorder public class NarayanaJtaRecorder { @@ -110,6 +111,30 @@ public void setConfig(final TransactionManagerConfiguration transactions) { .setXaResourceOrphanFilterClassNames(transactions.xaResourceOrphanFilters); } + /** + * This should be removed in the future. + */ + @Deprecated(forRemoval = true) + public void allowUnsafeMultipleLastResources(boolean agroalPresent, boolean disableMultipleLastResourcesWarning) { + arjPropertyManager.getCoreEnvironmentBean().setAllowMultipleLastResources(true); + arjPropertyManager.getCoreEnvironmentBean().setDisableMultipleLastResourcesWarning(disableMultipleLastResourcesWarning); + if (agroalPresent) { + jtaPropertyManager.getJTAEnvironmentBean() + .setLastResourceOptimisationInterfaceClassName("io.agroal.narayana.LocalXAResource"); + } + } + + /** + * This should be removed in the future. + */ + @Deprecated(forRemoval = true) + public void logUnsafeMultipleLastResourcesOnStartup( + TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode mode) { + log.warnf( + "Setting quarkus.transaction-manager.unsafe-multiple-last-resources to '%s' makes adding multiple resources to the same transaction unsafe.", + StringUtil.hyphenate(mode.name()).replace('_', '-')); + } + private void setObjectStoreDir(String name, TransactionManagerConfiguration config) { BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, name).setObjectStoreDir(config.objectStore.directory); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java new file mode 100644 index 0000000000000..4fff308594ec0 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java @@ -0,0 +1,66 @@ +package io.quarkus.narayana.jta.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public final class TransactionManagerBuildTimeConfig { + /** + * Define the behavior when using multiple XA unaware resources in the same transactional demarcation. + *

+ * Defaults to {@code warn-first}. + * {@code warn-first}, {@code warn-each} and {@code allow} are UNSAFE and should only be used for compatibility. + * Either use XA for all resources if you want consistency, or split the code into separate + * methods with separate transactions. + *

+ * Note that using a single XA unaware resource together with XA aware resources, known as + * the Last Resource Commit Optimization (LRCO), is different from using multiple XA unaware + * resources. Although LRCO allows most transactions to complete normally, some errors can + * cause an inconsistent transaction outcome. Using multiple XA unaware resources is not + * recommended since the probability of inconsistent outcomes is significantly higher and + * much harder to recover from than LRCO. For this reason, use LRCO as a last resort. + *

+ * We do not recommend using this configuration property, and we plan to remove it in the future, + * so you should plan fixing your application accordingly. + * If you think your use case of this feature is valid and this option should be kept around, + * open an issue in our tracker explaining why. + * + * @deprecated This property is planned for removal in a future version. + */ + @Deprecated(forRemoval = true) + @ConfigItem(defaultValueDocumentation = "warn-first") + public Optional unsafeMultipleLastResources; + + public enum UnsafeMultipleLastResourcesMode { + /** + * Allow using multiple XA unaware resources in the same transactional demarcation. + *

+ * This will log a warning once on application startup, + * but not on each use of multiple XA unaware resources in the same transactional demarcation. + */ + ALLOW, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on the first occurrence. + */ + WARN_FIRST, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on each occurrence. + */ + WARN_EACH, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on each occurrence. + */ + FAIL; + + // The default is WARN_FIRST in Quarkus 3.8, FAIL in Quarkus 3.9+ + // Make sure to update defaultValueDocumentation on unsafeMultipleLastResources when changing this. + public static final UnsafeMultipleLastResourcesMode DEFAULT = WARN_FIRST; + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java new file mode 100644 index 0000000000000..1e32c4ec2a9d7 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java @@ -0,0 +1,43 @@ +package io.quarkus.narayana.jta.runtime.graal; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.graalvm.nativeimage.hosted.Feature; + +/** + * Disables logging during the analysis phase + */ +public class DisableLoggingFeature implements Feature { + + private static final String[] CATEGORIES = { + "com.arjuna.ats.arjuna" + }; + + private final Map categoryMap = new HashMap<>(CATEGORIES.length); + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + for (String category : CATEGORIES) { + Logger logger = Logger.getLogger(category); + categoryMap.put(category, logger.getLevel()); + logger.setLevel(Level.SEVERE); + } + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + for (String category : CATEGORIES) { + Level level = categoryMap.remove(category); + Logger logger = Logger.getLogger(category); + logger.setLevel(level); + } + } + + @Override + public String getDescription() { + return "Disables INFO and WARN logging during the analysis phase"; + } +} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index e7971fdf3014d..e468b6860497f 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -33,7 +33,7 @@ public class DevServicesConfig { * ends with `-legacy`. * Override with `quarkus.keycloak.devservices.keycloak-x-image`. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.7") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:24.0.4") public String imageName; /** diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index c2ef04ff3d074..f7f2e1cdff083 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -109,7 +109,8 @@ public class KeycloakDevServicesProcessor { private static final String KEYCLOAK_QUARKUS_HOSTNAME = "KC_HOSTNAME"; private static final String KEYCLOAK_QUARKUS_ADMIN_PROP = "KEYCLOAK_ADMIN"; private static final String KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP = "KEYCLOAK_ADMIN_PASSWORD"; - private static final String KEYCLOAK_QUARKUS_START_CMD = "start --http-enabled=true --hostname-strict=false --hostname-strict-https=false"; + private static final String KEYCLOAK_QUARKUS_START_CMD = "start --http-enabled=true --hostname-strict=false --hostname-strict-https=false " + + "--spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json"; private static final String JAVA_OPTS = "JAVA_OPTS"; private static final String OIDC_USERS = "oidc.users"; @@ -509,6 +510,7 @@ protected void configure() { addEnv(KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD); withCommand(startCommand.orElse(KEYCLOAK_QUARKUS_START_CMD) + (useSharedNetwork ? " --hostname-port=" + fixedExposedPort.getAsInt() : "")); + addUpConfigResource(); } else { addEnv(KEYCLOAK_WILDFLY_USER_PROP, KEYCLOAK_ADMIN_USER); addEnv(KEYCLOAK_WILDFLY_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD); @@ -560,6 +562,13 @@ private void mapResource(String resourcePath, String mappedResource) { } } + private void addUpConfigResource() { + if (Thread.currentThread().getContextClassLoader().getResource("/dev-service/upconfig.json") != null) { + LOG.debug("Mapping the classpath /dev-service/upconfig.json resource to /opt/keycloak/upconfig.json"); + withClasspathResourceMapping("/dev-service/upconfig.json", "/opt/keycloak/upconfig.json", BindMode.READ_ONLY); + } + } + private Integer findRandomPort() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); diff --git a/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json b/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json new file mode 100644 index 0000000000000..8487089bc90fd --- /dev/null +++ b/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json @@ -0,0 +1,60 @@ +{ + "attributes": [ + { + "name": "username", + "displayName": "${username}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "min": 3, "max": 255 }, + "username-prohibited-characters": {}, + "up-username-not-idn-homograph": {} + } + }, + { + "name": "email", + "displayName": "${email}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "email" : {}, + "length": { "max": 255 } + } + }, + { + "name": "firstName", + "displayName": "${firstName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + }, + { + "name": "lastName", + "displayName": "${lastName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + } + ], + "groups": [ + { + "name": "user-metadata", + "displayHeader": "User metadata", + "displayDescription": "Attributes, which refer to user metadata" + } + ] +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index d9d73cadab57b..3ab5ca1e57776 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -8,14 +8,12 @@ import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toMap; -import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.UncheckedIOException; import java.lang.reflect.Modifier; import java.nio.charset.Charset; -import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -39,7 +37,6 @@ import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -91,12 +88,15 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.fs.util.ZipUtils; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.maven.dependency.Dependency; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; +import io.quarkus.paths.FilteredPathTree; +import io.quarkus.paths.PathFilter; +import io.quarkus.paths.PathTree; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.Engine; import io.quarkus.qute.EngineBuilder; @@ -2108,9 +2108,6 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives, QuteConfig config, TemplateRootsBuildItem templateRoots) throws IOException { - Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); - List extensionArtifacts = curateOutcome.getApplicationModel().getDependencies().stream() - .filter(Dependency::isRuntimeExtensionArtifact).collect(Collectors.toList()); // Make sure the new templates are watched as well watchedPaths.produce(HotDeploymentWatchedFileBuildItem.builder().setLocationPredicate(new Predicate() { @@ -2125,71 +2122,39 @@ public boolean test(String path) { } }).build()); - for (ResolvedDependency artifact : extensionArtifacts) { - if (isApplicationArchive(artifact, allApplicationArchives)) { - // Skip extension archives that are also application archives - continue; - } - for (Path resolvedPath : artifact.getResolvedPaths()) { - if (Files.isDirectory(resolvedPath)) { - scanPath(resolvedPath, resolvedPath, config, templateRoots, watchedPaths, templatePaths, - nativeImageResources); - } else { - try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) { - for (String templateRoot : templateRoots) { - Path artifactBasePath = artifactFs.getPath(templateRoot); - if (Files.exists(artifactBasePath)) { - LOGGER.debugf("Found extension templates in: %s", resolvedPath); - scan(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, templatePaths, - nativeImageResources, - config); - } - } - } catch (IOException e) { - LOGGER.warnf(e, "Unable to create the file system from the path: %s", resolvedPath); - } - } + final Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); + final Set appArtifactKeys = new HashSet<>(allApplicationArchives.size()); + for (var archive : allApplicationArchives) { + appArtifactKeys.add(archive.getKey()); + } + for (ResolvedDependency artifact : curateOutcome.getApplicationModel() + .getDependencies(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT)) { + // Skip extension archives that are also application archives + if (!appArtifactKeys.contains(artifact.getKey())) { + scanPathTree(artifact.getContentTree(), templateRoots, watchedPaths, templatePaths, nativeImageResources, + config); } } for (ApplicationArchive archive : allApplicationArchives) { - archive.accept(tree -> { - for (Path root : tree.getRoots()) { - // Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect - // a wrong directory name on case-insensitive file systems - scanPath(root, root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); - } - }); + archive.accept( + tree -> scanPathTree(tree, templateRoots, watchedPaths, templatePaths, nativeImageResources, config)); } } - private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, + private void scanPathTree(PathTree pathTree, TemplateRootsBuildItem templateRoots, BuildProducer watchedPaths, BuildProducer templatePaths, - BuildProducer nativeImageResources) { - if (!Files.isDirectory(path)) { - return; - } - try (Stream paths = Files.list(path)) { - for (Path file : paths.collect(Collectors.toList())) { - if (Files.isDirectory(file)) { - // Iterate over the directories in the root - // "/io", "/META-INF", "/templates", "/web", etc. - Path relativePath = rootPath.relativize(file); - if (templateRoots.isRoot(relativePath)) { - LOGGER.debugf("Found templates dir: %s", file); - // The base path is an OS-specific path relative to the template root - String basePath = relativePath.toString() + File.separatorChar; - scan(file, file, basePath, watchedPaths, templatePaths, - nativeImageResources, - config); - } else if (templateRoots.maybeRoot(relativePath)) { - // Scan the path recursively because the template root may be nested, for example "/web/public" - scanPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); - } + BuildProducer nativeImageResources, + QuteConfig config) { + for (String templateRoot : templateRoots) { + pathTree.accept(templateRoot, visit -> { + if (visit != null) { + // if template root is found in this tree then walk over its subtree + scanTemplateRootSubtree( + new FilteredPathTree(pathTree, PathFilter.forIncludes(List.of(templateRoot + "/**"))), + visit.getRelativePath(), watchedPaths, templatePaths, nativeImageResources, config); } - } - } catch (IOException e) { - throw new UncheckedIOException(e); + }); } } @@ -3367,69 +3332,58 @@ public static String getName(InjectionPointInfo injectionPoint) { throw new IllegalArgumentException(); } + /** + * + * @param templatePaths + * @param watchedPaths + * @param nativeImageResources + * @param resourcePath The relative resource path, including the template root + * @param templatePath The path relative to the template root; using the {@code /} path separator + * @param originalPath + * @param config + */ private static void produceTemplateBuildItems(BuildProducer templatePaths, BuildProducer watchedPaths, - BuildProducer nativeImageResources, String basePath, String filePath, + BuildProducer nativeImageResources, String resourcePath, + String templatePath, Path originalPath, QuteConfig config) { - if (filePath.isEmpty()) { + if (templatePath.isEmpty()) { return; } - // OS-specific full path, i.e. templates\foo.html - String osSpecificPath = basePath + filePath; - // OS-agnostic full path, i.e. templates/foo.html - String osAgnosticPath = osSpecificPath; - if (File.separatorChar != '/') { - osAgnosticPath = osAgnosticPath.replace(File.separatorChar, '/'); - } - LOGGER.debugf("Produce template build items [filePath: %s, fullPath: %s, originalPath: %s", filePath, osSpecificPath, + LOGGER.debugf("Produce template build items [templatePath: %s, osSpecificResourcePath: %s, originalPath: %s", + templatePath, + resourcePath, originalPath); boolean restartNeeded = true; if (config.devMode.noRestartTemplates.isPresent()) { - restartNeeded = !config.devMode.noRestartTemplates.get().matcher(osAgnosticPath).matches(); + restartNeeded = !config.devMode.noRestartTemplates.get().matcher(resourcePath).matches(); } - watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(osAgnosticPath, restartNeeded)); - nativeImageResources.produce(new NativeImageResourceBuildItem(osSpecificPath)); + watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(resourcePath, restartNeeded)); + nativeImageResources.produce(new NativeImageResourceBuildItem(resourcePath)); templatePaths.produce( - new TemplatePathBuildItem(filePath, originalPath, readTemplateContent(originalPath, config.defaultCharset))); + new TemplatePathBuildItem(templatePath, originalPath, + readTemplateContent(originalPath, config.defaultCharset))); } - private void scan(Path root, Path directory, String basePath, BuildProducer watchedPaths, + private void scanTemplateRootSubtree(PathTree pathTree, String templateRoot, + BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, - QuteConfig config) - throws IOException { - try (Stream files = Files.list(directory)) { - Iterator iter = files.iterator(); - while (iter.hasNext()) { - Path filePath = iter.next(); - /* - * Fix for https://github.com/quarkusio/quarkus/issues/25751 where running tests in Eclipse - * sometimes produces `/templates/tags` (absolute) files listed for `templates` (relative) - * directories, so we work around this - */ - if (!directory.isAbsolute() - && filePath.isAbsolute() - && filePath.getRoot() != null) { - filePath = filePath.getRoot().relativize(filePath); - } - if (Files.isRegularFile(filePath)) { - LOGGER.debugf("Found template: %s", filePath); - String templatePath = root.relativize(filePath).toString(); - if (File.separatorChar != '/') { - templatePath = templatePath.replace(File.separatorChar, '/'); - } - if (config.templatePathExclude.matcher(templatePath).matches()) { - LOGGER.debugf("Template file excluded: %s", filePath); - continue; - } - produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, basePath, templatePath, - filePath, config); - } else if (Files.isDirectory(filePath)) { - LOGGER.debugf("Scan directory: %s", filePath); - scan(root, filePath, basePath, watchedPaths, templatePaths, nativeImageResources, config); - } + QuteConfig config) { + pathTree.walk(visit -> { + if (Files.isRegularFile(visit.getPath())) { + LOGGER.debugf("Found template: %s", visit.getPath()); + // remove templateRoot + / + final String relativePath = visit.getRelativePath(); + String templatePath = relativePath.substring(templateRoot.length() + 1); + if (config.templatePathExclude.matcher(templatePath).matches()) { + LOGGER.debugf("Template file excluded: %s", visit.getPath()); + return; + } + produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, + relativePath, templatePath, visit.getPath(), config); } - } + }); } private static boolean isExcluded(TypeCheck check, Iterable> excludes) { @@ -3460,19 +3414,6 @@ private void checkDuplicatePaths(List templatePaths) { } } - private boolean isApplicationArchive(ResolvedDependency dependency, Set applicationArchives) { - for (ApplicationArchive archive : applicationArchives) { - if (archive.getKey() == null) { - continue; - } - if (dependency.getGroupId().equals(archive.getKey().getGroupId()) - && dependency.getArtifactId().equals(archive.getKey().getArtifactId())) { - return true; - } - } - return false; - } - static String readTemplateContent(Path path, Charset defaultCharset) { try { return Files.readString(path, defaultCharset); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java index 7e26be68d7834..37b3606d7a096 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java @@ -28,6 +28,7 @@ public class AdditionalTemplateRootTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot(root -> root .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt") + .addAsResource(new StringAsset("Hoho {name}!"), "templates/nested/hoho.txt") .addAsResource(new StringAsset("Hello {name}!"), "web/public/hello.txt")) .addBuildChainCustomizer(buildCustomizer()); @@ -50,13 +51,12 @@ public void execute(BuildContext context) { List items = context.consumeMulti(NativeImageResourceBuildItem.class); for (NativeImageResourceBuildItem item : items) { if (item.getResources().contains("web/public/hello.txt") - || item.getResources().contains("web\\public\\hello.txt") || item.getResources().contains("templates/hi.txt") - || item.getResources().contains("templates\\hi.txt")) { + || item.getResources().contains("templates/nested/hoho.txt")) { found++; } } - if (found != 2) { + if (found != 3) { throw new IllegalStateException(items.stream().flatMap(i -> i.getResources().stream()) .collect(Collectors.toList()).toString()); } @@ -79,6 +79,7 @@ public void execute(BuildContext context) { public void testTemplate() { assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render()); assertEquals("Hello M!", hello.data("name", "M").render()); + assertEquals("Hoho M!", engine.getTemplate("nested/hoho").data("name", "M").render()); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/UserFriendlyQuarkusRESTCapabilityCombinationTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/UserFriendlyQuarkusRESTCapabilityCombinationTest.java new file mode 100644 index 0000000000000..7e4ac075bfd59 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/UserFriendlyQuarkusRESTCapabilityCombinationTest.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.Version; +import io.quarkus.deployment.Capability; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +class UserFriendlyQuarkusRESTCapabilityCombinationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-resteasy-reactive-deployment", Version.getVersion()))) + .assertException(t -> { + assertTrue(t.getMessage().contains("only one provider of the following capabilities"), t.getMessage()); + assertTrue(t.getMessage().contains("capability %s is provided by".formatted(Capability.REST)), t.getMessage()); + }); + + @Test + public void test() { + fail(); + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index 1bab91fc08c5d..d19971eee9dd8 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.jackson.deployment.processor; +import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.JSON_IGNORE; import static io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem.isSecurityConfigExpressionCandidate; import static org.jboss.resteasy.reactive.common.util.RestMediaType.APPLICATION_NDJSON; import static org.jboss.resteasy.reactive.common.util.RestMediaType.APPLICATION_STREAM_JSON; @@ -15,6 +16,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.function.Supplier; import jakarta.inject.Singleton; @@ -59,6 +61,7 @@ import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; +import io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames; import io.quarkus.resteasy.reactive.common.deployment.ResourceScanningResultBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem; import io.quarkus.resteasy.reactive.jackson.CustomDeserialization; @@ -372,7 +375,12 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r JaxRsResourceIndexBuildItem index, BuildProducer producer) { IndexView indexView = index.getIndexView(); - Map typeToHasSecureField = new HashMap<>(); + boolean noSecureFieldDetected = indexView.getAnnotations(SECURE_FIELD).isEmpty(); + if (noSecureFieldDetected) { + return; + } + + Map typeToHasSecureField = new HashMap<>(getTypesWithSecureField()); List result = new ArrayList<>(); for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : resourceMethodEntries.getEntries()) { MethodInfo methodInfo = entry.getMethodInfo(); @@ -425,7 +433,7 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r } ClassInfo effectiveReturnClassInfo = indexView.getClassByName(effectiveReturnType.name()); - if ((effectiveReturnClassInfo == null) || effectiveReturnClassInfo.name().equals(ResteasyReactiveDotNames.OBJECT)) { + if (effectiveReturnClassInfo == null) { continue; } AtomicBoolean needToDeleteCache = new AtomicBoolean(false); @@ -443,6 +451,7 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r } if (needToDeleteCache.get()) { typeToHasSecureField.clear(); + typeToHasSecureField.putAll(getTypesWithSecureField()); } } if (!result.isEmpty()) { @@ -452,6 +461,13 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r } } + private static Map getTypesWithSecureField() { + // if any of following types is detected as an endpoint return type or a field of endpoint return type, + // we always need to apply security serialization as any type can be represented with them + return Map.of(ResteasyReactiveDotNames.OBJECT.toString(), Boolean.TRUE, ResteasyReactiveDotNames.RESPONSE.toString(), + Boolean.TRUE); + } + private static boolean hasSecureFields(IndexView indexView, ClassInfo currentClassInfo, Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { // use cached result if there is any @@ -479,10 +495,20 @@ private static boolean hasSecureFields(IndexView indexView, ClassInfo currentCla .anyMatch(ci -> hasSecureFields(indexView, ci, typeToHasSecureField, needToDeleteCache)); } else { // figure if any field or parent / subclass field is secured - hasSecureFields = hasSecureFields(currentClassInfo) - || anyFieldHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache) - || anySubclassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache) - || anyParentClassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache); + if (hasSecureFields(currentClassInfo)) { + hasSecureFields = true; + } else { + Predicate ignoredTypesPredicate = QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE; + if (ignoredTypesPredicate.test(currentClassInfo.name())) { + hasSecureFields = false; + } else { + hasSecureFields = anyFieldHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, + needToDeleteCache) + || anySubclassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache) + || anyParentClassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, + needToDeleteCache); + } + } } typeToHasSecureField.put(className, hasSecureFields); return hasSecureFields; @@ -513,6 +539,7 @@ private static boolean anyFieldHasSecureFields(IndexView indexView, ClassInfo cu return currentClassInfo .fields() .stream() + .filter(fieldInfo -> !fieldInfo.hasAnnotation(JSON_IGNORE)) .map(FieldInfo::type) .anyMatch(fieldType -> fieldTypeHasSecureFields(fieldType, indexView, typeToHasSecureField, needToDeleteCache)); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java index 6fd1e3c3553f8..3b8f1cf12509e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java @@ -17,11 +17,13 @@ import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; +import io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization; import io.smallrye.common.annotation.Blocking; @Path("/multipart") public class MultipartResource { + @DisableSecureSerialization // Person has @SecureField but we want to inspect all data @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -45,6 +47,7 @@ public Map greeting(@Valid @BeanParam FormData formData) { return result; } + @DisableSecureSerialization // Person has @SecureField but we want to inspect all data @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/ResponseType.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/ResponseType.java new file mode 100644 index 0000000000000..dc0235c8464a1 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/ResponseType.java @@ -0,0 +1,57 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; + +import io.quarkus.resteasy.reactive.jackson.SecureField; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public enum ResponseType { + /** + * Returns DTOs directly. + */ + PLAIN(true, "plain"), + /** + * Returns {@link Multi} with DTOs. + */ + // TODO: enable when https://github.com/quarkusio/quarkus/issues/40447 gets fixed + //MULTI(true, "multi"), + /** + * Returns {@link Uni} with DTOs. + */ + UNI(true, "uni"), + /** + * Returns {@link Object} that is either DTO with a {@link SecureField} or not. + */ + OBJECT(false, "object"), // we must always assume it can contain SecureField + /** + * Returns {@link Response} that is either DTO with a {@link SecureField} or not. + */ + RESPONSE(false, "response"), // we must always assume it can contain SecureField + /** + * Returns {@link RestResponse} with DTOs. + */ + REST_RESPONSE(true, "rest-response"), + /** + * Returns {@link RestResponse} with DTOs. + */ + COLLECTION(true, "collection"); + + private final boolean secureFieldDetectable; + private final String resourceSubPath; + + ResponseType(boolean secureFieldDetectable, String resourceSubPath) { + this.secureFieldDetectable = secureFieldDetectable; + this.resourceSubPath = resourceSubPath; + } + + boolean isSecureFieldDetectable() { + return secureFieldDetectable; + } + + String getResourceSubPath() { + return resourceSubPath; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecureFieldDetectionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecureFieldDetectionTest.java new file mode 100644 index 0000000000000..4c93d912b230a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecureFieldDetectionTest.java @@ -0,0 +1,399 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.MethodInfo; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.common.model.ResourceClass; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.ServerResourceMethod; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import io.quarkus.resteasy.reactive.jackson.SecureField; +import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; +import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class SecureFieldDetectionTest { + + private static final String SECURITY_SERIALIZATION = "security_serialization"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MultiResource.class, UniResource.class, ObjectResource.class, ResponseResource.class, + PlainResource.class, TestIdentityProvider.class, TestIdentityController.class, + CollectionResource.class, NoSecureField.class, WithSecureField.class, WithNestedSecureField.class, + ResponseType.class, DetectSecuritySerializationHandler.class, JsonIgnoreDto.class)) + .addBuildChainCustomizer(buildChainBuilder -> buildChainBuilder.addBuildStep(context -> context.produce( + new MethodScannerBuildItem(new MethodScanner() { + @Override + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + return List.of(new DetectSecuritySerializationHandler()); + } + }))).produces(MethodScannerBuildItem.class).build()); + + @BeforeEach + public void setupSecurity() { + TestIdentityController.resetRoles().add("Georgios", "Andrianakis", "admin"); + } + + private static Stream responseTypes() { + return EnumSet.allOf(ResponseType.class).stream().map(Enum::toString).map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("responseTypes") + public void testSecureFieldDetection(String responseTypeStr) { + var responseType = ResponseType.valueOf(responseTypeStr); + + // with auth + RestAssured + .given() + .auth().preemptive().basic("Georgios", "Andrianakis") + .pathParam("sub-path", responseType.getResourceSubPath()) + .get("/{sub-path}/secure-field") + .then() + .statusCode(200) + .body(containsString("hush hush")); + RestAssured + .given() + .auth().preemptive().basic("Georgios", "Andrianakis") + .pathParam("sub-path", responseType.getResourceSubPath()) + .get("/{sub-path}/no-secure-field") + .then() + .statusCode(200) + .body(containsString("public")); + + // no auth + RestAssured + .given() + .pathParam("sub-path", responseType.getResourceSubPath()) + .get("/{sub-path}/secure-field") + .then() + .statusCode(200) + .header(SECURITY_SERIALIZATION, is("true")) + .body(not(containsString("hush hush"))); + + // if endpoint returns for example Object or Response we can't really tell during the build time + // therefore we add custom security serialization and let decision be made dynamically based on present annotation + boolean isSecureSerializationApplied = !responseType.isSecureFieldDetectable(); + RestAssured + .given() + .pathParam("sub-path", responseType.getResourceSubPath()) + .get("/{sub-path}/no-secure-field") + .then() + .statusCode(200) + .header(SECURITY_SERIALIZATION, is(Boolean.toString(isSecureSerializationApplied))) + .body(containsString("public")); + + RestAssured + .given() + .pathParam("sub-path", responseType.getResourceSubPath()) + .get("/{sub-path}/json-ignore") + .then() + .statusCode(200) + .header(SECURITY_SERIALIZATION, is(Boolean.toString(isSecureSerializationApplied))) + .body(containsString("other")) + .body(not(containsString("ignored"))); + } + + @Path("plain") + public static class PlainResource { + + @Path("secure-field") + @GET + public WithNestedSecureField secureField() { + return createEntityWithSecureField(); + } + + @Path("no-secure-field") + @GET + public NoSecureField noSecureField() { + return createEntityWithoutSecureField(); + } + + @Path("json-ignore") + @GET + public JsonIgnoreDto jsonIgnore() { + return createEntityWithSecureFieldInIgnored(); + } + + } + + @Path("multi") + public static class MultiResource { + + @Path("secure-field") + @GET + public Multi secureField() { + return Multi.createFrom().item(createEntityWithSecureField()); + } + + @Path("no-secure-field") + @GET + public Multi noSecureField() { + return Multi.createFrom().item(createEntityWithoutSecureField()); + } + + @Path("json-ignore") + @GET + public Multi jsonIgnore() { + return Multi.createFrom().item(createEntityWithSecureFieldInIgnored()); + } + + } + + @Path("collection") + public static class CollectionResource { + + @Path("secure-field") + @GET + public Collection secureField() { + return List.of(createEntityWithSecureField()); + } + + @Path("no-secure-field") + @GET + public Collection noSecureField() { + return Set.of(createEntityWithoutSecureField()); + } + + @Path("json-ignore") + @GET + public Collection jsonIgnore() { + return Set.of(createEntityWithSecureFieldInIgnored()); + } + + } + + @Path("uni") + public static class UniResource { + + @Path("secure-field") + @GET + public Uni secureField() { + return Uni.createFrom().item(createEntityWithSecureField()); + } + + @Path("no-secure-field") + @GET + public Uni noSecureField() { + return Uni.createFrom().item(createEntityWithoutSecureField()); + } + + @Path("json-ignore") + @GET + public Uni jsonIgnore() { + return Uni.createFrom().item(createEntityWithSecureFieldInIgnored()); + } + + } + + @Produces(APPLICATION_JSON) + @Path("object") + public static class ObjectResource { + + @Path("secure-field") + @GET + public Object secureField() { + return createEntityWithSecureField(); + } + + @Path("no-secure-field") + @GET + public Object noSecureField() { + return createEntityWithoutSecureField(); + } + + @Path("json-ignore") + @GET + public Object jsonIgnore() { + return createEntityWithSecureFieldInIgnored(); + } + + } + + @Produces(APPLICATION_JSON) + @Path("response") + public static class ResponseResource { + + @Path("secure-field") + @GET + public Response secureField() { + return Response.ok(createEntityWithSecureField()).build(); + } + + @Path("no-secure-field") + @GET + public Response noSecureField() { + return Response.ok(createEntityWithoutSecureField()).build(); + } + + @Path("json-ignore") + @GET + public Response jsonIgnore() { + return Response.ok(createEntityWithSecureFieldInIgnored()).build(); + } + + } + + @Path("rest-response") + public static class RestResponseResource { + + @Path("secure-field") + @GET + public RestResponse secureField() { + return RestResponse.ok(createEntityWithSecureField()); + } + + @Path("no-secure-field") + @GET + public RestResponse noSecureField() { + return RestResponse.ok(createEntityWithoutSecureField()); + } + + @Path("json-ignore") + @GET + public RestResponse jsonIgnore() { + return RestResponse.ok(createEntityWithSecureFieldInIgnored()); + } + + } + + private static NoSecureField createEntityWithoutSecureField() { + var resp = new NoSecureField(); + resp.setNotSecured("public"); + return resp; + } + + private static JsonIgnoreDto createEntityWithSecureFieldInIgnored() { + var resp = new JsonIgnoreDto(); + resp.setOtherField("other"); + var nested = new WithSecureField(); + nested.setSecured("ignored"); + resp.setWithSecureField(nested); + return resp; + } + + private static WithNestedSecureField createEntityWithSecureField() { + var resp = new WithNestedSecureField(); + var nested = new WithSecureField(); + nested.setSecured("hush hush"); + resp.setWithSecureField(nested); + return resp; + } + + public static class JsonIgnoreDto { + + @JsonIgnore + private WithSecureField withSecureField; + + private String otherField; + + public WithSecureField getWithSecureField() { + return withSecureField; + } + + public void setWithSecureField(WithSecureField withSecureField) { + this.withSecureField = withSecureField; + } + + public String getOtherField() { + return otherField; + } + + public void setOtherField(String otherField) { + this.otherField = otherField; + } + } + + public static class NoSecureField { + + private String notSecured; + + public String getNotSecured() { + return notSecured; + } + + public void setNotSecured(String notSecured) { + this.notSecured = notSecured; + } + } + + public static class WithNestedSecureField { + + private WithSecureField withSecureField; + + public WithSecureField getWithSecureField() { + return withSecureField; + } + + public void setWithSecureField(WithSecureField withSecureField) { + this.withSecureField = withSecureField; + } + } + + public static class WithSecureField { + + @SecureField(rolesAllowed = "admin") + private String secured; + + public String getSecured() { + return secured; + } + + public void setSecured(String secured) { + this.secured = secured; + } + } + + public static class DetectSecuritySerializationHandler implements ServerRestHandler, HandlerChainCustomizer { + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + var methodId = requestContext.getResteasyReactiveResourceInfo().getMethodId(); + var customSerialization = ResteasyReactiveServerJacksonRecorder.customSerializationForMethod(methodId); + var customSerializationDetected = Boolean.toString(customSerialization != null); + requestContext.unwrap(RoutingContext.class).response().putHeader(SECURITY_SERIALIZATION, + customSerializationDetected); + } + + @Override + public List handlers(Phase phase, ResourceClass resourceClass, ServerResourceMethod resourceMethod) { + return List.of(new DetectSecuritySerializationHandler()); + } + } +} diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 8aa777f640b67..aae68a223e326 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -55,6 +55,7 @@ import io.quarkus.arc.processor.BuildExtension; import io.quarkus.arc.processor.ObserverInfo; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -518,6 +519,7 @@ void transformSecurityAnnotations(BuildProducer } } + @Consume(Capabilities.class) // make sure extension combinations are validated before default security check @BuildStep @Record(ExecutionTime.STATIC_INIT) void gatherSecurityChecks(BuildProducer syntheticBeans, @@ -528,7 +530,7 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, BuildProducer configBuilderProducer, List additionalSecuredMethods, SecurityCheckRecorder recorder, - Optional defaultSecurityCheckBuildItem, + List defaultSecurityCheckBuildItem, BuildProducer reflectiveClassBuildItemBuildProducer, List additionalSecurityChecks, SecurityBuildTimeConfig config) { classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate())); @@ -562,8 +564,14 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, methodEntry.getValue()); } - if (defaultSecurityCheckBuildItem.isPresent()) { - var roles = defaultSecurityCheckBuildItem.get().getRolesAllowed(); + if (!defaultSecurityCheckBuildItem.isEmpty()) { + if (defaultSecurityCheckBuildItem.size() > 1) { + int itemCount = defaultSecurityCheckBuildItem.size(); + throw new IllegalStateException("Found %d DefaultSecurityCheckBuildItem items, ".formatted(itemCount) + + "please make sure the item is produced exactly once"); + } + + var roles = defaultSecurityCheckBuildItem.get(0).getRolesAllowed(); if (roles == null) { recorder.registerDefaultSecurityCheck(builder, recorder.denyAll()); } else { diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java index ed3dafe18de0d..67765b5728cf1 100644 --- a/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java @@ -3,9 +3,16 @@ import java.util.List; import java.util.Objects; -import io.quarkus.builder.item.SimpleBuildItem; - -public final class DefaultSecurityCheckBuildItem extends SimpleBuildItem { +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Registers default SecurityCheck with the SecurityCheckStorage. + * Please make sure this build item is produced exactly once or validation will fail and exception will be thrown. + */ +public final class DefaultSecurityCheckBuildItem + // we make this Multi to run CapabilityAggregationStep#aggregateCapabilities first + // so that user-friendly error message is logged when Quarkus REST and RESTEasy are used together + extends MultiBuildItem { public final List rolesAllowed; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java index a2349c3024766..47bde335702f4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java @@ -87,6 +87,11 @@ public void accept(String relativePath, Consumer func) { delegate.accept(relativePath, func); } + @Override + public void acceptAll(String relativePath, Consumer func) { + delegate.acceptAll(relativePath, func); + } + @Override public boolean contains(String relativePath) { final LinkedHashMap snapshot = walkSnapshot; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java index 11e125a04fbf1..8265ef59771f1 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java @@ -54,6 +54,15 @@ public void accept(String relativePath, Consumer consumer) { } } + @Override + public void acceptAll(String relativePath, Consumer consumer) { + if (!PathFilter.isVisible(filter, relativePath)) { + consumer.accept(null); + } else { + original.acceptAll(relativePath, consumer); + } + } + @Override public boolean contains(String relativePath) { return PathFilter.isVisible(filter, relativePath) && original.contains(relativePath); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java index 4681cec199deb..e09b70ce12168 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java @@ -86,6 +86,26 @@ public void accept(PathVisit t) { } } + @Override + public void acceptAll(String relativePath, Consumer func) { + final AtomicBoolean consumed = new AtomicBoolean(); + final Consumer wrapper = new Consumer<>() { + @Override + public void accept(PathVisit t) { + if (t != null) { + func.accept(t); + consumed.set(true); + } + } + }; + for (PathTree tree : trees) { + tree.accept(relativePath, wrapper); + } + if (!consumed.get()) { + func.accept(null); + } + } + @Override public boolean contains(String relativePath) { for (PathTree tree : trees) { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index b126db5924d26..5b68db900bc93 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -134,6 +134,20 @@ default boolean isEmpty() { */ void accept(String relativePath, Consumer consumer); + /** + * Consumes a given path relative to the root of the tree. + * If the path isn't found in the tree, the {@link PathVisit} argument + * passed to the consumer will be {@code null}. + * + * If multiple items match then the consumer will be called multiple times. + * + * @param relativePath relative path to consume + * @param consumer path consumer + */ + default void acceptAll(String relativePath, Consumer consumer) { + accept(relativePath, consumer); + } + /** * Checks whether the tree contains a relative path. * diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathVisit.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathVisit.java index 139560a3d40f1..5084405c4bdcf 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathVisit.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathVisit.java @@ -44,6 +44,16 @@ default URL getUrl() { } } + /** + * Path relative to the root of the tree as a string with {@code /} as a path element separator. + * This method calls {@link #getRelativePath(String)} passing {@code /} as an argument. + * + * @return path relative to the root of the tree as a string with {@code /} as a path element separator + */ + default String getRelativePath() { + return getRelativePath("/"); + } + /** * Path relative to the root of the tree as a string with a provided path element separator. * For a {@link PathTree} created for an archive, the returned path will be relative to the root diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java index b5cd7e9cdd3a8..1206da6cc618c 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java @@ -158,6 +158,11 @@ public void accept(String relativePath, Consumer consumer) { delegate.accept(relativePath, consumer); } + @Override + public void acceptAll(String relativePath, Consumer consumer) { + delegate.acceptAll(relativePath, consumer); + } + @Override public boolean contains(String relativePath) { return delegate.contains(relativePath); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java index eefce68aa5f66..2378ecdfafb70 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java @@ -4,6 +4,7 @@ import java.nio.file.Path; import java.security.ProtectionDomain; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.jar.Manifest; @@ -129,4 +130,9 @@ public void close() { } }; + + default List getResources(String name) { + ClassPathResource resource = getResource(name); + return resource == null ? List.of() : List.of(resource); + } } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java index 6f05de1707e71..61bf42a4cb9a5 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java @@ -12,8 +12,10 @@ import java.security.CodeSource; import java.security.ProtectionDomain; import java.security.cert.Certificate; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; @@ -106,6 +108,26 @@ public ClassPathResource getResource(String name) { return apply(tree -> tree.apply(sanitized, visit -> visit == null ? null : new Resource(visit))); } + @Override + public List getResources(String name) { + final String sanitized = sanitize(name); + final Set resources = this.resources; + if (resources != null && !resources.contains(sanitized)) { + return List.of(); + } + List ret = new ArrayList<>(); + apply(tree -> { + tree.acceptAll(sanitized, visit -> { + if (visit != null) { + ret.add(new Resource(visit)); + + } + }); + return List.of(); + }); + return ret; + } + @Override public T apply(Function func) { lock.readLock().lock(); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index e1b20fe85d657..1f1c3bf2c1cdf 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -11,6 +11,7 @@ import java.sql.Driver; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -246,27 +247,31 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea if (providers != null) { boolean endsWithTrailingSlash = unsanitisedName.endsWith("/"); for (ClassPathElement element : providers) { - ClassPathResource res = element.getResource(name); + Collection resList = element.getResources(name); //if the requested name ends with a trailing / we make sure //that the resource is a directory, and return a URL that ends with a / //this matches the behaviour of URLClassLoader - if (endsWithTrailingSlash) { - if (res.isDirectory()) { - try { - resources.add(new URL(res.getUrl().toString() + "/")); - } catch (MalformedURLException e) { - throw new RuntimeException(e); + for (var res : resList) { + if (endsWithTrailingSlash) { + if (res.isDirectory()) { + try { + resources.add(new URL(res.getUrl().toString() + "/")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } } + } else { + resources.add(res.getUrl()); } - } else { - resources.add(res.getUrl()); } } } else if (name.isEmpty()) { for (ClassPathElement i : elements) { - ClassPathResource res = i.getResource(""); - if (res != null) { - resources.add(res.getUrl()); + List resList = i.getResources(""); + for (var res : resList) { + if (res != null) { + resources.add(res.getUrl()); + } } } } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java index 97f6c8f1456d4..556407a142017 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java @@ -232,7 +232,9 @@ private static Properties loadPomProps(Path appJar, Path artifactIdPath) throws } public static Model readModel(final Path pomXml) throws IOException { - return readModel(Files.newInputStream(pomXml)); + Model model = readModel(Files.newInputStream(pomXml)); + model.setPomFile(pomXml.toFile()); + return model; } public static Model readModel(InputStream stream) throws IOException { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java index 657599ca1a957..4d288748ef627 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java @@ -640,7 +640,9 @@ protected void handleUnrecoverableError(Throwable throwable) { protected void endResponse() { if (serverResponse().headWritten()) { - serverRequest().closeConnection(); + if (!serverResponse().closed()) { + serverRequest().closeConnection(); + } } else { serverResponse().setStatusCode(500).end(); } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java index 0e02e22bb1b00..d6a097ed92312 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java @@ -406,7 +406,7 @@ public void removeResponseHeader(String name) { @Override public boolean closed() { - return response.closed(); + return response.ended() || response.closed(); } @Override diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute index eade03aa1a939..286a7757dbc7c 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.18 +FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.19 ENV LANGUAGE='en_US:en' diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java index 4b7d518029d31..a449bb3bd80f5 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java @@ -86,12 +86,12 @@ public enum LayoutType { public static final String DEFAULT_QUARKIVERSE_PARENT_GROUP_ID = "io.quarkiverse"; public static final String DEFAULT_QUARKIVERSE_PARENT_ARTIFACT_ID = "quarkiverse-parent"; - public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "15"; + public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "16"; public static final String DEFAULT_QUARKIVERSE_NAMESPACE_ID = "quarkus-"; public static final String DEFAULT_QUARKIVERSE_GUIDE_URL = "https://quarkiverse.github.io/quarkiverse-docs/%s/dev/"; private static final String DEFAULT_SUREFIRE_PLUGIN_VERSION = "3.2.5"; - private static final String DEFAULT_COMPILER_PLUGIN_VERSION = "3.12.1"; + private static final String DEFAULT_COMPILER_PLUGIN_VERSION = "3.13.0"; private final QuarkusExtensionCodestartProjectInputBuilder builder = QuarkusExtensionCodestartProjectInput.builder(); private final Path baseDir; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java index cc0a2169469c6..8cecb28fedae0 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java @@ -381,7 +381,7 @@ protected void refreshData() { return; } try { - model = ModelUtils.readModel(projectPom); + model = MojoUtils.readPom(projectPom.toFile()); } catch (IOException e) { throw new RuntimeException("Failed to read " + projectPom, e); } diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java index 9e760539d8f5c..c65cd1d3b1aa3 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java @@ -320,13 +320,13 @@ private void checkDockerfilesWithMaven(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./mvnw package")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./mvnw package -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) @@ -346,13 +346,13 @@ private void checkDockerfilesWithGradle(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./gradlew build")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./gradlew build -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/keycloak-authorization/src/main/resources/application.properties b/integration-tests/keycloak-authorization/src/main/resources/application.properties index a8714fb1b906a..8acc066c5a0a1 100644 --- a/integration-tests/keycloak-authorization/src/main/resources/application.properties +++ b/integration-tests/keycloak-authorization/src/main/resources/application.properties @@ -113,3 +113,5 @@ admin-url=${keycloak.url} # Configure Keycloak Admin Client quarkus.keycloak.admin-client.server-url=${admin-url} + +quarkus.log.category."com.gargoylesoftware.htmlunit".level=ERROR diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm index 192010559a8c9..423791b5a44b9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.16 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm index fd5272297c2ef..c3dd7f16cdc5d 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm @@ -75,7 +75,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar index 95ce7681973c5..27e101d04d41e 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar @@ -75,7 +75,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml index faa8505392b3a..e591f173fef92 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml @@ -5,7 +5,7 @@ io.quarkiverse quarkiverse-parent - 15 + 16 io.quarkiverse.my-quarkiverse-ext quarkus-my-quarkiverse-ext-parent @@ -26,7 +26,7 @@ - 3.12.1 + 3.13.0 17 UTF-8 UTF-8 diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml index 5fb4073ede2a6..857ee512a7a4c 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml @@ -13,7 +13,7 @@ - 3.12.1 + 3.13.0 ${surefire-plugin.version} 17 UTF-8 diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index 60661ac0c9935..53c3417702c90 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -1,4 +1,5 @@ quarkus.keycloak.devservices.create-realm=false +quarkus.keycloak.devservices.show-logs=true # Default tenant configurationf quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index 3f386b7933edb..d55ba117f264c 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -125,7 +125,7 @@ quarkus.oidc.tenant-public-key.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCg smallrye.jwt.sign.key.location=/privateKey.pem smallrye.jwt.new-token.lifespan=5 -quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."com.gargoylesoftware.htmlunit".level=ERROR quarkus.http.auth.proactive=false quarkus.native.additional-build-args=-H:IncludeResources=.*\\.pem diff --git a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java index 29a9327e6d87b..bcd717025d989 100644 --- a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java +++ b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java @@ -53,7 +53,7 @@ public void testGetUserNameWithAccessTokenPropagation() { //.statusCode(200) //.body(equalTo("alice")); .statusCode(500) - .body(containsString("Feature not enabled")); + .body(containsString("Client not allowed to exchange")); } @Test diff --git a/integration-tests/oidc/src/main/resources/upconfig.json b/integration-tests/oidc/src/main/resources/upconfig.json new file mode 100644 index 0000000000000..8487089bc90fd --- /dev/null +++ b/integration-tests/oidc/src/main/resources/upconfig.json @@ -0,0 +1,60 @@ +{ + "attributes": [ + { + "name": "username", + "displayName": "${username}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "min": 3, "max": 255 }, + "username-prohibited-characters": {}, + "up-username-not-idn-homograph": {} + } + }, + { + "name": "email", + "displayName": "${email}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "email" : {}, + "length": { "max": 255 } + } + }, + { + "name": "firstName", + "displayName": "${firstName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + }, + { + "name": "lastName", + "displayName": "${lastName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + } + ], + "groups": [ + { + "name": "user-metadata", + "displayHeader": "User metadata", + "displayDescription": "Attributes, which refer to user metadata" + } + ] +} \ No newline at end of file diff --git a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java index 21c76533a6334..abe4321c0789d 100644 --- a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java +++ b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java @@ -51,10 +51,12 @@ public Map start() { keycloak = keycloak .withClasspathResourceMapping(SERVER_KEYSTORE, SERVER_KEYSTORE_MOUNTED_PATH, BindMode.READ_ONLY) .withClasspathResourceMapping(SERVER_TRUSTSTORE, SERVER_TRUSTSTORE_MOUNTED_PATH, BindMode.READ_ONLY) + .withClasspathResourceMapping("/upconfig.json", "/opt/keycloak/upconfig.json", BindMode.READ_ONLY) .withCommand("build --https-client-auth=required") .withCommand(String.format( "start --https-client-auth=required --hostname-strict=false --hostname-strict-https=false" - + " --https-key-store-file=%s --https-trust-store-file=%s --https-trust-store-password=password", + + " --https-key-store-file=%s --https-trust-store-file=%s --https-trust-store-password=password" + + " --spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json", SERVER_KEYSTORE_MOUNTED_PATH, SERVER_TRUSTSTORE_MOUNTED_PATH)); keycloak.start(); LOGGER.info(keycloak.getLogs()); diff --git a/integration-tests/openapi/pom.xml b/integration-tests/openapi/pom.xml index e4c6f8c826deb..4183a3d64635b 100644 --- a/integration-tests/openapi/pom.xml +++ b/integration-tests/openapi/pom.xml @@ -52,6 +52,11 @@ assertj-core test + + io.quarkus + quarkus-test-security + test + diff --git a/integration-tests/openapi/src/main/java/io/quarkus/it/openapi/security/TestSecurityResource.java b/integration-tests/openapi/src/main/java/io/quarkus/it/openapi/security/TestSecurityResource.java new file mode 100644 index 0000000000000..7676535bd3864 --- /dev/null +++ b/integration-tests/openapi/src/main/java/io/quarkus/it/openapi/security/TestSecurityResource.java @@ -0,0 +1,29 @@ +package io.quarkus.it.openapi.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; + +import io.quarkus.vertx.web.RouteFilter; +import io.vertx.ext.web.RoutingContext; + +@Path("/security") +public class TestSecurityResource { + + @RolesAllowed("admin") + @GET + @Path("reactive-routes") + public String reactiveRoutes(@Context SecurityContext securityContext) { + return securityContext.getUserPrincipal().getName(); + } + + @RouteFilter(401) + public void doNothing(RoutingContext routingContext) { + // here so that the Reactive Routes extension activates CDI request context + routingContext.response().putHeader("reactive-routes-filter", "true"); + routingContext.next(); + } + +} diff --git a/integration-tests/openapi/src/test/java/io/quarkus/it/openapi/security/TestSecurityReactiveRoutesTest.java b/integration-tests/openapi/src/test/java/io/quarkus/it/openapi/security/TestSecurityReactiveRoutesTest.java new file mode 100644 index 0000000000000..ca6870dbe86ad --- /dev/null +++ b/integration-tests/openapi/src/test/java/io/quarkus/it/openapi/security/TestSecurityReactiveRoutesTest.java @@ -0,0 +1,26 @@ +package io.quarkus.it.openapi.security; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; + +@QuarkusTest +@TestHTTPEndpoint(TestSecurityResource.class) +public class TestSecurityReactiveRoutesTest { + + @TestSecurity(user = "Martin", roles = "admin") + @Test + public void testSecurityWithReactiveRoutesAndQuarkusRest() { + RestAssured.get("reactive-routes") + .then() + .statusCode(200) + .header("reactive-routes-filter", is("true")) + .body(is("Martin")); + } + +} diff --git a/integration-tests/test-extension/tests/src/main/resources/io/quarkus/it/extension/my_resource.txt b/integration-tests/test-extension/tests/src/main/resources/io/quarkus/it/extension/my_resource.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java new file mode 100644 index 0000000000000..b863c448676ee --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java @@ -0,0 +1,29 @@ +package io.quarkus.it.extension; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class ClassLoaderTest { + + @Test + void testClassLoaderResources() throws IOException { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + ArrayList resources = Collections.list(contextClassLoader.getResources("io/quarkus/it/extension")); + Assertions.assertEquals(2, resources.size()); + } + + @Test + void testClassLoaderSingleResource() throws IOException { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + URL resource = contextClassLoader.getResource("io/quarkus/it/extension/my_resource.txt"); + Assertions.assertNotNull(resource); + } +} diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java index f77201d07bf6b..61e2faa9db89c 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java @@ -74,7 +74,7 @@ public class QuarkusProdModeTest implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, TestWatcher, InvocationInterceptor { - private static final String EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED = "features"; + private static final String EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED = "Installed features"; private static final int DEFAULT_HTTP_PORT_INT = 8081; private static final String DEFAULT_HTTP_PORT = "" + DEFAULT_HTTP_PORT_INT; private static final String QUARKUS_HTTP_PORT_PROPERTY = "quarkus.http.port"; diff --git a/test-framework/junit5-internal/src/test/java/io/quarkus/test/QuarkusProdModeTestConfusingLogTest.java b/test-framework/junit5-internal/src/test/java/io/quarkus/test/QuarkusProdModeTestConfusingLogTest.java new file mode 100644 index 0000000000000..2450c61632824 --- /dev/null +++ b/test-framework/junit5-internal/src/test/java/io/quarkus/test/QuarkusProdModeTestConfusingLogTest.java @@ -0,0 +1,110 @@ +package io.quarkus.test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.sun.net.httpserver.HttpServer; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.annotations.QuarkusMain; + +public class QuarkusProdModeTestConfusingLogTest { + + @RegisterExtension + static final QuarkusProdModeTest simpleApp = new QuarkusProdModeTest() + .withApplicationRoot(jar -> jar.addClass(Main.class)) + .setApplicationName("simple-app") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true); + + static HttpClient client; + + @BeforeAll + static void setUp() { + // No tear down, because there's no way to shut down the client explicitly before Java 21 :( + // We'll just hope no connection is left hanging. + client = HttpClient.newBuilder() + .connectTimeout(Duration.ofMillis(100)) + .build(); + } + + @Test + public void shouldWaitForAppActuallyStarted() { + thenAppIsRunning(); + + whenStopApp(); + thenAppIsNotRunning(); + + whenStartApp(); + thenAppIsRunning(); + } + + private void whenStopApp() { + simpleApp.stop(); + } + + private void whenStartApp() { + simpleApp.start(); + } + + private void thenAppIsNotRunning() { + assertNotNull(simpleApp.getExitCode(), "App is running"); + assertThrows(IOException.class, this::tryReachApp, "App's HTTP server is still running"); + } + + private void thenAppIsRunning() { + assertNull(simpleApp.getExitCode(), "App is not running"); + assertDoesNotThrow(this::tryReachApp, "App's HTTP server is not reachable"); + } + + private void tryReachApp() throws IOException, InterruptedException { + String response = client.send(HttpRequest.newBuilder().uri(URI.create("http://localhost:8081/test")).GET().build(), + HttpResponse.BodyHandlers.ofString()) + .body(); + // If the app is reachable, this is the expected response. + assertEquals("OK", response, "App returned unexpected response"); + } + + @QuarkusMain + public static class Main { + public static void main(String[] args) { + // Use an unrelated log to trick QuarkusProdModeTest into thinking the app started + System.out.println( + "HHH000511: The -9999.-9999.-9999 version for [org.hibernate.dialect.PostgreSQLDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 12.0.0. Check the community dialects project for available legacy versions."); + try { + // Delay the actual app start so there's a decent chance of QuarkusProdModeTest + // being ahead of the app -- otherwise we wouldn't reproduce the bug. + Thread.sleep(500); + // Expose an endpoint proving the app is up + HttpServer server = HttpServer.create(new InetSocketAddress(8081), 0); + server.createContext("/test", exchange -> { + String response = "OK"; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + }); + server.start(); + Quarkus.run(args); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + } +}