diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index 3323c0824516a..bb61fbcea28ae 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -1290,7 +1290,13 @@ static void filterJarFile(Path resolvedDep, Path targetPath, Set transfo } else { manifest = new Manifest(); } - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath), manifest)) { + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath))) { + JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME); + // Set manifest time to epoch to always make the same jar + manifestEntry.setTime(0); + out.putNextEntry(manifestEntry); + manifest.write(out); + out.closeEntry(); Enumeration entries = in.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); @@ -1306,6 +1312,8 @@ static void filterJarFile(Path resolvedDep, Path targetPath, Set transfo while ((r = inStream.read(buffer)) > 0) { out.write(buffer, 0, r); } + } finally { + out.closeEntry(); } } else { log.debugf("Removed %s from %s", entryName, resolvedDep); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java index 9e50d306377ff..fc5180b34e9a7 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java @@ -68,6 +68,26 @@ void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception { } } + @Test + void manifestTimeShouldAlwaysBeSetToEpoch(@TempDir Path tempDir) throws Exception { + JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "myarchive.jar") + .addClasses(Integer.class) + .addManifest(); + Path initialJar = tempDir.resolve("initial.jar"); + Path filteredJar = tempDir.resolve("filtered.jar"); + archive.as(ZipExporter.class).exportTo(new File(initialJar.toUri()), true); + JarResultBuildStep.filterJarFile(initialJar, filteredJar, Set.of("java/lang/Integer.class")); + try (JarFile jarFile = new JarFile(filteredJar.toFile())) { + assertThat(jarFile.stream()) + .filteredOn(jarEntry -> jarEntry.getName().equals(JarFile.MANIFEST_NAME)) + .isNotEmpty() + .allMatch(jarEntry -> jarEntry.getTime() == 0); + // Check that the manifest is still has attributes + Manifest manifest = jarFile.getManifest(); + assertThat(manifest.getMainAttributes()).isNotEmpty(); + } + } + private static KeyStore.PrivateKeyEntry createPrivateKeyEntry() throws NoSuchAlgorithmException, CertificateException, OperatorCreationException, CertIOException { KeyPairGenerator ky = KeyPairGenerator.getInstance("RSA"); diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index 3b5515a5f7691..d7e5900a76e91 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -84,10 +84,7 @@ Create a REST endpoint in `src/main/java/org/acme/security/jwt/TokenSecuredResou ---- package org.acme.security.jwt; -import java.security.Principal; - import jakarta.annotation.security.PermitAll; -import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; @@ -105,7 +102,7 @@ public class TokenSecuredResource { @Inject JsonWebToken jwt; // <1> - @GET() + @GET @Path("permit-all") @PermitAll // <2> @Produces(MediaType.TEXT_PLAIN) @@ -122,7 +119,7 @@ public class TokenSecuredResource { } else { name = ctx.getUserPrincipal().getName(); // <6> } - return String.format("hello + %s," + return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", @@ -172,7 +169,7 @@ Now that the REST endpoint is running, we can access it using a command line too [source,shell] ---- $ curl http://127.0.0.1:8080/secured/permit-all; echo -hello + anonymous, isHttps: false, authScheme: null, hasJWT: false +hello anonymous, isHttps: false, authScheme: null, hasJWT: false ---- We have not provided any JWT in our request, so we would not expect that there is any security state seen by the endpoint, @@ -194,7 +191,6 @@ package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; -import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; @@ -207,7 +203,6 @@ import jakarta.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") -@RequestScoped public class TokenSecuredResource { @Inject @@ -238,7 +233,7 @@ public class TokenSecuredResource { } else { name = ctx.getUserPrincipal().getName(); } - return String.format("hello + %s," + return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", @@ -455,7 +450,7 @@ curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUI [source,shell] ---- $ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo -hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13 +hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13 ---- Success! We now have: @@ -500,14 +495,14 @@ import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") -@RequestScoped +@RequestScoped <1> public class TokenSecuredResource { @Inject - JsonWebToken jwt; // <1> + JsonWebToken jwt; // <2> @Inject @Claim(standard = Claims.birthdate) - String birthdate; // <2> + String birthdate; // <3> @GET @Path("permit-all") @@ -530,7 +525,7 @@ public class TokenSecuredResource { @RolesAllowed("Admin") @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowedAdmin(@Context SecurityContext ctx) { - return getResponseString(ctx) + ", birthdate: " + birthdate; // <3> + return getResponseString(ctx) + ", birthdate: " + birthdate; // <4> } private String getResponseString(SecurityContext ctx) { @@ -542,7 +537,7 @@ public class TokenSecuredResource { } else { name = ctx.getUserPrincipal().getName(); } - return String.format("hello + %s," + return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", @@ -554,9 +549,10 @@ public class TokenSecuredResource { } } ---- -<1> Here we inject the JsonWebToken. -<2> Here we inject the `birthday` claim as `String` - this is why the `@RequestScoped` scope is now required. -<3> Here we use the injected `birthday` claim to build the final reply. +<1> `RequestScoped` scope is required to support an injection of the `birthday` claim as `String`. +<2> Here we inject the JsonWebToken. +<3> Here we inject the `birthday` claim as `String` - this is why the `@RequestScoped` scope is now required. +<4> Here we use the injected `birthday` claim to build the final reply. Now generate the token again and run: @@ -568,7 +564,7 @@ curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUI [source,shell] ---- $ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo -hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13 +hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13 ---- === Package and run the application diff --git a/docs/src/main/asciidoc/tls-registry-reference.adoc b/docs/src/main/asciidoc/tls-registry-reference.adoc index db480222b323d..8774141b5c93e 100644 --- a/docs/src/main/asciidoc/tls-registry-reference.adoc +++ b/docs/src/main/asciidoc/tls-registry-reference.adoc @@ -25,7 +25,7 @@ The TLS Registry extension is automatically included in your project when you us As a result, applications that use the TLS Registry can be ready to handle secure communications out of the box. TLS Registry also provides features like automatic certificate reloading, Let's Encrypt (ACME) integration, Kubernetes Cert-Manager support, and compatibility with various keystore formats, such as PKCS12, PEM, and JKS. -[#using-the-tls-registry] +[[using-the-tls-registry]] == Using the TLS registry To configure a TLS connection, including key and truststores, use the `+quarkus.tls.*+` properties. @@ -126,7 +126,7 @@ quarkus.grpc.server.plain-text=false + This configuration enables mTLS by ensuring that both the server and client validate each other's certificates, which provides an additional layer of security. -[#referencing-a-tls-configuration] +[[referencing-a-tls-configuration]] == Referencing a TLS configuration To reference an example _named_ configuration that you created by using the `quarkus.tls..*` properties as explained in <> @@ -257,7 +257,7 @@ quarkus.tls.key-store.jks.alias-password=my-alias-password * Alternatively, use SNI to select the appropriate certificate and private key. Note that all keys must use the same password. -[#sni] +[[sni]] ==== SNI Server Name Indication (SNI) is a TLS extension that makes it possible for a client to specify the host name to which it attempts to connect during the TLS handshake. @@ -585,7 +585,7 @@ When an application that uses the TLS extension starts, the TLS registry perform If any of these checks fail, the application will not start. -[#reloading-certificates] +[[reloading-certificates]] == Reloading certificates The `TlsConfiguration` obtained from the `TLSConfigurationRegistry` includes a mechanism for reloading certificates. @@ -1267,12 +1267,11 @@ quarkus.http.insecure-requests=redirect ==== -[[lets-encrypt-prepare]] - The challenge is served from the primary HTTP interface (accessible from your DNS domain name). IMPORTANT: Do not start your application yet. +[[lets-encrypt-prepare]] === Application preparation Before you request a Let's Encrypt certificate: diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 03ee25fd6e059..792edd0eba119 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -83,9 +83,11 @@ import io.smallrye.graphql.api.Entry; import io.smallrye.graphql.api.ErrorExtensionProvider; import io.smallrye.graphql.api.OneOf; +import io.smallrye.graphql.api.federation.Authenticated; import io.smallrye.graphql.api.federation.ComposeDirective; import io.smallrye.graphql.api.federation.Extends; import io.smallrye.graphql.api.federation.External; +import io.smallrye.graphql.api.federation.FieldSet; import io.smallrye.graphql.api.federation.Inaccessible; import io.smallrye.graphql.api.federation.InterfaceObject; import io.smallrye.graphql.api.federation.Key; @@ -93,6 +95,15 @@ import io.smallrye.graphql.api.federation.Requires; import io.smallrye.graphql.api.federation.Shareable; import io.smallrye.graphql.api.federation.Tag; +import io.smallrye.graphql.api.federation.link.Import; +import io.smallrye.graphql.api.federation.link.Link; +import io.smallrye.graphql.api.federation.link.Purpose; +import io.smallrye.graphql.api.federation.policy.Policy; +import io.smallrye.graphql.api.federation.policy.PolicyGroup; +import io.smallrye.graphql.api.federation.policy.PolicyItem; +import io.smallrye.graphql.api.federation.requiresscopes.RequiresScopes; +import io.smallrye.graphql.api.federation.requiresscopes.ScopeGroup; +import io.smallrye.graphql.api.federation.requiresscopes.ScopeItem; import io.smallrye.graphql.cdi.config.MicroProfileConfig; import io.smallrye.graphql.cdi.producer.GraphQLProducer; import io.smallrye.graphql.cdi.tracing.TracingService; @@ -298,6 +309,17 @@ void buildFinalIndex( indexer.indexClass(io.smallrye.graphql.api.federation.Override.class); indexer.indexClass(Tag.class); indexer.indexClass(OneOf.class); + indexer.indexClass(Authenticated.class); + indexer.indexClass(FieldSet.class); + indexer.indexClass(Link.class); + indexer.indexClass(Import.class); + indexer.indexClass(Purpose.class); + indexer.indexClass(Policy.class); + indexer.indexClass(PolicyGroup.class); + indexer.indexClass(PolicyItem.class); + indexer.indexClass(RequiresScopes.class); + indexer.indexClass(ScopeGroup.class); + indexer.indexClass(ScopeItem.class); } catch (IOException ex) { LOG.warn("Failure while creating index", ex); } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js index 67eee71367278..5a3b9e8fcb2bc 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -136,6 +136,7 @@ export class QwcServerLog extends QwcAbstractLogElement { connectedCallback() { super.connectedCallback(); this._toggleOnOff(true); + this._history(); this._loadAllLoggers(); } @@ -744,4 +745,4 @@ export class QwcServerLog extends QwcAbstractLogElement { } -customElements.define('qwc-server-log', QwcServerLog); \ No newline at end of file +customElements.define('qwc-server-log', QwcServerLog); diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java index 8c515413071a7..07add0fad2b0a 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.JarFile; @@ -14,7 +13,8 @@ public class JarFileReference { // This is required to perform cleanup of JarResource::jarFileReference without breaking racy updates - private CompletableFuture completedFuture; + private final CompletableFuture completedFuture; + // Guarded by an atomic reader counter that emulate the behaviour of a read/write lock. // To enable virtual threads compatibility and avoid pinning it is not possible to use an explicit read/write lock // because the jarFile access may happen inside a native call (for example triggered by the RunnerClassLoader) @@ -26,22 +26,10 @@ public class JarFileReference { // The JarFileReference is created as already acquired and that's why the referenceCounter starts from 2 private final AtomicInteger referenceCounter = new AtomicInteger(2); - private JarFileReference(JarFile jarFile) { + private JarFileReference(JarFile jarFile, CompletableFuture completedFuture) { this.jarFile = jarFile; - } - - public static JarFileReference completeWith(CompletableFuture completableFuture, JarFile jarFile) { - Objects.requireNonNull(completableFuture); - var jarFileRef = new JarFileReference(jarFile); - jarFileRef.completedFuture = completableFuture; - completableFuture.complete(jarFileRef); - return jarFileRef; - } - - public static CompletableFuture completedWith(JarFile jarFile) { - var jarFileRef = new JarFileReference(jarFile); - jarFileRef.completedFuture = CompletableFuture.completedFuture(jarFileRef); - return jarFileRef.completedFuture; + this.completedFuture = completedFuture; + this.completedFuture.complete(this); } /** @@ -57,21 +45,20 @@ private boolean acquire() { if (count == 0) { return false; } - if (referenceCounter.compareAndSet(count, addCount(count, 1))) { + if (referenceCounter.compareAndSet(count, changeReferenceCount(count, 1))) { return true; } } } /** - * This is not allowed to change the sign of count (unless put it to 0) + * Change the absolute value of the provided reference count of the given delta, that can only be 1 when the reference is + * acquired by a new reader or -1 when the reader releases the reference or the reference itself is marked for closing. + * A negative reference count means that this reference has been marked for closing. */ - private static int addCount(final int count, int delta) { + private static int changeReferenceCount(final int count, int delta) { assert count != 0; - if (count < 0) { - delta = -delta; - } - return count + delta; + return count < 0 ? count - delta : count + delta; } /** @@ -89,9 +76,10 @@ private boolean release(JarResource jarResource) { if (count == 1 || count == 0) { throw new IllegalStateException("Duplicate release? The reference counter cannot be " + count); } - if (referenceCounter.compareAndSet(count, addCount(count, -1))) { + if (referenceCounter.compareAndSet(count, changeReferenceCount(count, -1))) { if (count == -1) { - silentCloseJarResources(jarResource); + // The reference has been already marked to be closed (the counter is negative) and this is the last reader releasing it + closeJarResources(jarResource); return true; } return false; @@ -99,7 +87,7 @@ private boolean release(JarResource jarResource) { } } - private void silentCloseJarResources(JarResource jarResource) { + private void closeJarResources(JarResource jarResource) { // we need to make sure we're not deleting others state jarResource.jarFileReference.compareAndSet(completedFuture, null); try { @@ -110,7 +98,7 @@ private void silentCloseJarResources(JarResource jarResource) { } /** - * Ask to close this reference. + * Mark this jar reference as ready to be closed. * If there are no readers currently accessing the jarFile also close it, otherwise defer the closing when the last reader * will leave. */ @@ -122,9 +110,10 @@ void markForClosing(JarResource jarResource) { return; } // close must change the value into a negative one or zeroing - if (referenceCounter.compareAndSet(count, addCount(-count, -1))) { + // the reference counter is turned into a negative value to indicate (in an idempotent way) that the resource has been marked to be closed. + if (referenceCounter.compareAndSet(count, changeReferenceCount(-count, -1))) { if (count == 1) { - silentCloseJarResources(jarResource); + closeJarResources(jarResource); } } } @@ -145,6 +134,7 @@ static T withJarFile(JarResource jarResource, String resource, JarFileConsum if (jarFileReference.acquire()) { return consumeSharedJarFile(jarFileReference, jarResource, resource, fileConsumer); } + // The acquire failure implies that the reference is already marked to be closed. closingLocalJarFileRef = true; } @@ -199,7 +189,8 @@ private static T consumeUnsharedJarFile(CompletableFuture private static CompletableFuture syncLoadAcquiredJarFile(JarResource jarResource) { try { - return JarFileReference.completedWith(JarFiles.create(jarResource.jarPath.toFile())); + return new JarFileReference(JarFiles.create(jarResource.jarPath.toFile()), + new CompletableFuture<>()).completedFuture; } catch (IOException e) { throw new RuntimeException("Failed to open " + jarResource.jarPath, e); } @@ -213,7 +204,7 @@ private static JarFileReference asyncLoadAcquiredJarFile(JarResource jarResource do { if (jarResource.jarFileReference.compareAndSet(null, newJarRefFuture)) { try { - return JarFileReference.completeWith(newJarRefFuture, JarFiles.create(jarResource.jarPath.toFile())); + return new JarFileReference(JarFiles.create(jarResource.jarPath.toFile()), newJarRefFuture); } catch (IOException e) { throw new RuntimeException(e); }