From 286f3904d1c659176f354a9563c769798d451dd2 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 20 Dec 2022 13:33:10 +0000 Subject: [PATCH 01/17] Reimplement CSRF feature as ServerRequestFilter with form read (cherry picked from commit 1ab36ed7904b9a64b01174567acd1a82f30da895) --- .../csrf/reactive/CsrfReactiveBuildStep.java | 48 +------ .../csrf/reactive/runtime/CsrfRecorder.java | 19 --- ...=> CsrfRequestResponseReactiveFilter.java} | 124 +++++++++++------- .../reactive/runtime/CsrfResponseFilter.java | 104 --------------- .../java/io/quarkus/it/csrf/TestResource.java | 18 +++ .../src/main/resources/application.properties | 2 +- .../templates/csrfTokenWithFormRead.html | 17 +++ .../io/quarkus/it/csrf/CsrfReactiveTest.java | 44 +++++++ 8 files changed, 163 insertions(+), 213 deletions(-) delete mode 100644 extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java rename extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/{CsrfHandler.java => CsrfRequestResponseReactiveFilter.java} (64%) delete mode 100644 extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java create mode 100644 integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html diff --git a/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java b/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java index 1028bfac08b5e..886289a8c1810 100644 --- a/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java +++ b/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java @@ -1,31 +1,15 @@ package io.quarkus.csrf.reactive; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.function.BooleanSupplier; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.MethodInfo; -import org.jboss.resteasy.reactive.server.model.FixedHandlersChainCustomizer; -import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; -import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; - import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.csrf.reactive.runtime.CsrfHandler; -import io.quarkus.csrf.reactive.runtime.CsrfReactiveConfig; -import io.quarkus.csrf.reactive.runtime.CsrfRecorder; -import io.quarkus.csrf.reactive.runtime.CsrfResponseFilter; +import io.quarkus.csrf.reactive.runtime.CsrfRequestResponseReactiveFilter; import io.quarkus.csrf.reactive.runtime.CsrfTokenParameterProvider; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem; -import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; @BuildSteps(onlyIf = CsrfReactiveBuildStep.IsEnabled.class) public class CsrfReactiveBuildStep { @@ -34,33 +18,11 @@ public class CsrfReactiveBuildStep { void registerProvider(BuildProducer additionalBeans, BuildProducer reflectiveClass, BuildProducer additionalIndexedClassesBuildItem) { - additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfResponseFilter.class)); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, CsrfResponseFilter.class)); - additionalIndexedClassesBuildItem - .produce(new AdditionalIndexedClassesBuildItem(CsrfResponseFilter.class.getName())); - + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfRequestResponseReactiveFilter.class)); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, CsrfRequestResponseReactiveFilter.class)); additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfTokenParameterProvider.class)); - } - - @BuildStep - public MethodScannerBuildItem configureHandler() { - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public List scan(MethodInfo method, ClassInfo actualEndpointClass, - Map methodContext) { - return Collections.singletonList( - new FixedHandlersChainCustomizer( - List.of(new CsrfHandler()), - HandlerChainCustomizer.Phase.BEFORE_METHOD_INVOKE)); - } - }); - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public HandlerConfigurationProviderBuildItem applyRuntimeConfig(CsrfRecorder recorder, - CsrfReactiveConfig csrfReactiveConfig) { - return new HandlerConfigurationProviderBuildItem(CsrfReactiveConfig.class, recorder.configure(csrfReactiveConfig)); + additionalIndexedClassesBuildItem + .produce(new AdditionalIndexedClassesBuildItem(CsrfRequestResponseReactiveFilter.class.getName())); } public static class IsEnabled implements BooleanSupplier { diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java deleted file mode 100644 index f1580c9b023dd..0000000000000 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.csrf.reactive.runtime; - -import java.util.function.Supplier; - -import io.quarkus.runtime.annotations.Recorder; - -@Recorder -public class CsrfRecorder { - - public Supplier configure(CsrfReactiveConfig csrfReactiveConfig) { - return new Supplier() { - @Override - public CsrfReactiveConfig get() { - return csrfReactiveConfig; - } - }; - } - -} diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java similarity index 64% rename from extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java rename to extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java index ac8f2c340fdf7..29e7b1458c65c 100644 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java +++ b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java @@ -1,23 +1,29 @@ package io.quarkus.csrf.reactive.runtime; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; import java.security.SecureRandom; import java.util.Base64; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.spi.GenericRuntimeConfigurableServerRestHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import io.vertx.core.http.Cookie; +import io.vertx.core.http.impl.CookieImpl; +import io.vertx.core.http.impl.ServerCookie; import io.vertx.ext.web.RoutingContext; -public class CsrfHandler implements GenericRuntimeConfigurableServerRestHandler { - private static final Logger LOG = Logger.getLogger(CsrfHandler.class); +public class CsrfRequestResponseReactiveFilter { + private static final Logger LOG = Logger.getLogger(CsrfRequestResponseReactiveFilter.class); /** * CSRF token key. @@ -30,27 +36,12 @@ public class CsrfHandler implements GenericRuntimeConfigurableServerRestHandler< */ private static final String CSRF_TOKEN_VERIFIED = "csrf_token_verified"; - // although technically the field does not need to be volatile (since the access mode is determined by the VarHandle use) - // it is a recommended practice by Doug Lea meant to catch cases where the field is accessed directly (by accident) - @SuppressWarnings("unused") - private volatile SecureRandom secureRandom; - - // use a VarHandle to access the secureRandom as the value is written only by the main thread - // and all other threads simply read the value, and thus we can use the Release / Acquire access mode - private static final VarHandle SECURE_RANDOM_VH; - - static { - try { - SECURE_RANDOM_VH = MethodHandles.lookup().findVarHandle(CsrfHandler.class, "secureRandom", - SecureRandom.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new Error(e); - } - } + private final SecureRandom secureRandom = new SecureRandom(); - private CsrfReactiveConfig config; + @Inject + Instance configInstance; - public CsrfHandler() { + public CsrfRequestResponseReactiveFilter() { } /** @@ -68,10 +59,10 @@ public CsrfHandler() { * {@value #CSRF_TOKEN_KEY} and value that is equal to the one supplied in the cookie. * */ - public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { - final ContainerRequestContext requestContext = reactiveRequestContext.getContainerRequestContext(); - - final RoutingContext routing = reactiveRequestContext.serverRequest().unwrap(RoutingContext.class); + @ServerRequestFilter + @WithFormRead + public void filter(ResteasyReactiveContainerRequestContext requestContext, RoutingContext routing) { + final CsrfReactiveConfig config = this.configInstance.get(); String cookieToken = getCookieToken(routing, config); if (cookieToken != null) { @@ -99,7 +90,7 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { if (cookieToken == null && isCsrfTokenRequired(routing, config)) { // Set the CSRF cookie with a randomly generated value byte[] tokenBytes = new byte[config.tokenSize]; - getSecureRandom().nextBytes(tokenBytes); + secureRandom.nextBytes(tokenBytes); routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes); routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes)); } @@ -115,7 +106,6 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { } else { LOG.debugf("Request has the media type: %s, skipping the token verification", requestContext.getMediaType().toString()); - requestContext.abortWith(badClientRequest()); return; } } @@ -132,8 +122,9 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { return; } - String csrfToken = (String) reactiveRequestContext.getFormParameter(config.formFieldName, true, true); - + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) requestContext + .getServerRequestContext(); + String csrfToken = (String) rrContext.getFormParameter(config.formFieldName, true, false); if (csrfToken == null) { LOG.debug("CSRF token is not found"); requestContext.abortWith(badClientRequest()); @@ -148,6 +139,7 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { return; } else { routing.put(CSRF_TOKEN_VERIFIED, true); + return; } } } else if (cookieToken == null) { @@ -156,10 +148,6 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { } } - private SecureRandom getSecureRandom() { - return (SecureRandom) SECURE_RANDOM_VH.getAcquire(this); - } - private static boolean isMatchingMediaType(MediaType contentType, MediaType expectedType) { return contentType.getType().equals(expectedType.getType()) && contentType.getSubtype().equals(expectedType.getSubtype()); @@ -169,6 +157,47 @@ private static Response badClientRequest() { return Response.status(400).build(); } + /** + * If the requirements below are true, sets a cookie by the name {@value #CSRF_TOKEN_KEY} that contains a CSRF token. + *
    + *
  • The request method is {@code GET}.
  • + *
  • The request does not contain a valid CSRF token cookie.
  • + *
+ * + * @throws IllegalStateException if the {@link RoutingContext} does not have a value for the key {@value #CSRF_TOKEN_KEY} + * and a cookie needs to be set. + */ + @ServerResponseFilter + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext, RoutingContext routing) { + final CsrfReactiveConfig config = configInstance.get(); + if (requestContext.getMethod().equals("GET") && isCsrfTokenRequired(routing, config) + && getCookieToken(routing, config) == null) { + + String cookieValue = null; + if (config.tokenSignatureKey.isPresent()) { + byte[] csrfTokenBytes = (byte[]) routing.get(CSRF_TOKEN_BYTES_KEY); + + if (csrfTokenBytes == null) { + throw new IllegalStateException( + "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); + } + cookieValue = CsrfTokenUtils.signCsrfToken(csrfTokenBytes, config.tokenSignatureKey.get()); + } else { + String csrfToken = (String) routing.get(CSRF_TOKEN_KEY); + + if (csrfToken == null) { + throw new IllegalStateException( + "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); + } + cookieValue = csrfToken; + } + + createCookie(cookieValue, routing, config); + } + + } + /** * Gets the CSRF token from the CSRF cookie from the current {@code RoutingContext}. * @@ -189,6 +218,19 @@ private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig c return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; } + private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { + + ServerCookie cookie = new CookieImpl(config.cookieName, csrfToken); + cookie.setHttpOnly(true); + cookie.setSecure(config.cookieForceSecure || routing.request().isSSL()); + cookie.setMaxAge(config.cookieMaxAge.toSeconds()); + cookie.setPath(config.cookiePath); + if (config.cookieDomain.isPresent()) { + cookie.setDomain(config.cookieDomain.get()); + } + routing.response().addCookie(cookie); + } + private static boolean requestMethodIsSafe(ContainerRequestContext context) { switch (context.getMethod()) { case "GET": @@ -199,14 +241,4 @@ private static boolean requestMethodIsSafe(ContainerRequestContext context) { return false; } } - - public void configure(CsrfReactiveConfig configuration) { - this.config = configuration; - SECURE_RANDOM_VH.setRelease(this, new SecureRandom()); - } - - @Override - public Class getConfigurationClass() { - return CsrfReactiveConfig.class; - } } diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java deleted file mode 100644 index 7cb34dbd5eaa7..0000000000000 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.quarkus.csrf.reactive.runtime; - -import javax.enterprise.inject.Instance; -import javax.inject.Inject; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.server.ServerResponseFilter; - -import io.vertx.core.http.Cookie; -import io.vertx.core.http.impl.CookieImpl; -import io.vertx.core.http.impl.ServerCookie; -import io.vertx.ext.web.RoutingContext; - -public class CsrfResponseFilter { - private static final Logger LOG = Logger.getLogger(CsrfResponseFilter.class); - - /** - * CSRF token key. - */ - private static final String CSRF_TOKEN_KEY = "csrf_token"; - private static final String CSRF_TOKEN_BYTES_KEY = "csrf_token_bytes"; - - @Inject - Instance config; - - public CsrfResponseFilter() { - } - - /** - * If the requirements below are true, sets a cookie by the name {@value #CSRF_TOKEN_KEY} that contains a CSRF token. - *
    - *
  • The request method is {@code GET}.
  • - *
  • The request does not contain a valid CSRF token cookie.
  • - *
- * - * @throws IllegalStateException if the {@link RoutingContext} does not have a value for the key {@value #CSRF_TOKEN_KEY} - * and a cookie needs to be set. - */ - @ServerResponseFilter - public void filter(ContainerRequestContext requestContext, - ContainerResponseContext responseContext, RoutingContext routing) { - if (requestContext.getMethod().equals("GET") && isCsrfTokenRequired(routing, config.get()) - && getCookieToken(routing, config.get()) == null) { - - String cookieValue = null; - if (config.get().tokenSignatureKey.isPresent()) { - byte[] csrfTokenBytes = (byte[]) routing.get(CSRF_TOKEN_BYTES_KEY); - - if (csrfTokenBytes == null) { - throw new IllegalStateException( - "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); - } - cookieValue = CsrfTokenUtils.signCsrfToken(csrfTokenBytes, config.get().tokenSignatureKey.get()); - } else { - String csrfToken = (String) routing.get(CSRF_TOKEN_KEY); - - if (csrfToken == null) { - throw new IllegalStateException( - "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); - } - cookieValue = csrfToken; - } - - createCookie(cookieValue, routing, config.get()); - } - - } - - /** - * Gets the CSRF token from the CSRF cookie from the current {@code RoutingContext}. - * - * @return An Optional containing the token, or an empty Optional if the token cookie is not present or is invalid - */ - private String getCookieToken(RoutingContext routing, CsrfReactiveConfig config) { - Cookie cookie = routing.getCookie(config.cookieName); - - if (cookie == null) { - LOG.debug("CSRF token cookie is not set"); - return null; - } - - return cookie.getValue(); - } - - private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig config) { - return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; - } - - private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { - - ServerCookie cookie = new CookieImpl(config.cookieName, csrfToken); - cookie.setHttpOnly(true); - cookie.setSecure(config.cookieForceSecure || routing.request().isSSL()); - cookie.setMaxAge(config.cookieMaxAge.toSeconds()); - cookie.setPath(config.cookiePath); - if (config.cookieDomain.isPresent()) { - cookie.setDomain(config.cookieDomain.get()); - } - routing.response().addCookie(cookie); - } - -} diff --git a/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java b/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java index 9814e1d4fe4ae..d882897fcb82f 100644 --- a/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java +++ b/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java @@ -27,6 +27,9 @@ public class TestResource { @Inject Template csrfTokenForm; + @Inject + Template csrfTokenWithFormRead; + @Inject Template csrfTokenMultipart; @@ -40,6 +43,13 @@ public TemplateInstance getCsrfTokenForm() { return csrfTokenForm.instance(); } + @GET + @Path("/csrfTokenWithFormRead") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance getCsrfTokenWithFormRead() { + return csrfTokenWithFormRead.instance(); + } + @POST @Path("/csrfTokenForm") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -48,6 +58,14 @@ public String postCsrfTokenForm(@FormParam("name") String name) { return name + ":" + routingContext.get("csrf_token_verified", false); } + @POST + @Path("/csrfTokenWithFormRead") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + public String postCsrfTokenWithFormRead() { + return "verified:" + routingContext.get("csrf_token_verified", false); + } + @GET @Path("/csrfTokenMultipart") @Produces(MediaType.TEXT_HTML) diff --git a/integration-tests/csrf-reactive/src/main/resources/application.properties b/integration-tests/csrf-reactive/src/main/resources/application.properties index d84f200ebf014..add9b0a990af0 100644 --- a/integration-tests/csrf-reactive/src/main/resources/application.properties +++ b/integration-tests/csrf-reactive/src/main/resources/application.properties @@ -1,4 +1,4 @@ quarkus.csrf-reactive.cookie-name=csrftoken -quarkus.csrf-reactive.create-token-path=/service/csrfTokenForm,/service/csrfTokenMultipart +quarkus.csrf-reactive.create-token-path=/service/csrfTokenForm,/service/csrfTokenWithFormRead,/service/csrfTokenMultipart quarkus.csrf-reactive.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow diff --git a/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html b/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html new file mode 100644 index 0000000000000..d68a90c3849f3 --- /dev/null +++ b/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html @@ -0,0 +1,17 @@ + + + + +CSRF Token With Form Read Test + + +

CSRF Test

+ +
+ + +

Your Name:

+

+
+ + diff --git a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java index f1976362b4cac..9c0302b75f8da 100644 --- a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java +++ b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java @@ -46,6 +46,31 @@ public void testCsrfTokenInForm() throws Exception { } } + @Test + public void testCsrfTokenWithFormRead() throws Exception { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithFormRead"); + + assertEquals("CSRF Token With Form Read Test", htmlPage.getTitleText()); + + HtmlForm loginForm = htmlPage.getForms().get(0); + + loginForm.getInputByName("name").setValueAttribute("alice"); + + assertNotNull(webClient.getCookieManager().getCookie("csrftoken")); + + TextPage textPage = loginForm.getInputByName("submit").click(); + + assertEquals("verified:true", textPage.getContent()); + + textPage = webClient.getPage("http://localhost:8081/service/hello"); + assertEquals("hello", textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + @Test public void testCsrfTokenInFormButNoCookie() throws Exception { try (final WebClient webClient = createWebClient()) { @@ -148,6 +173,25 @@ public void testWrongCsrfTokenFormValue() throws Exception { } } + @Test + public void testWrongCsrfTokenWithFormRead() throws Exception { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithFormRead"); + + assertEquals("CSRF Token With Form Read Test", htmlPage.getTitleText()); + + assertNotNull(webClient.getCookieManager().getCookie("csrftoken")); + + RestAssured.given().urlEncodingEnabled(true) + .param("csrf-token", "wrong-value") + .post("/service/csrfTokenWithFormRead") + .then().statusCode(400); + + webClient.getCookieManager().clearCookies(); + } + } + private WebClient createWebClient() { WebClient webClient = new WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler()); From d1a0171e63e82ddb3163c18bc28061eaeec548cc Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 21 Dec 2022 11:10:18 +0100 Subject: [PATCH 02/17] Fix AbstractMethodError when injecting generated resource Fix https://github.com/quarkusio/quarkus/issues/29885 Plus, this pull request also resolves an ambiguous bean resolution when injecting these resources. (cherry picked from commit 0fd9456bb4d564072a7ef543e771b017b84d15a4) --- .../deployment/DataAccessImplementor.java | 19 +++++++++++++ .../EntityDataAccessImplementor.java | 23 ++++++++++++++++ .../RepositoryDataAccessImplementor.java | 23 ++++++++++++++++ .../deployment/ResourceImplementor.java | 21 +++++++++++++++ .../AbstractInjectResourcesMethodTest.java | 17 ++++++++++++ .../deployment/entity/InjectionResource.java | 26 ++++++++++++++++++ ...tityResourceInjectResourcesMethodTest.java | 19 +++++++++++++ .../deployment/DataAccessImplementor.java | 19 +++++++++++++ .../EntityDataAccessImplementor.java | 23 ++++++++++++++++ .../RepositoryDataAccessImplementor.java | 23 ++++++++++++++++ .../deployment/ResourceImplementor.java | 23 +++++++++++++++- .../AbstractInjectResourcesMethodTest.java | 17 ++++++++++++ .../deployment/entity/InjectionResource.java | 27 +++++++++++++++++++ ...tityResourceInjectResourcesMethodTest.java | 19 +++++++++++++ .../src/test/resources/application.properties | 2 +- .../deployment/JaxRsResourceImplementor.java | 8 ++++++ 16 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java create mode 100644 extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java create mode 100644 extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java create mode 100644 extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java create mode 100644 extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java create mode 100644 extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java index 6e42b89c706fd..b944fb334f02a 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java @@ -17,6 +17,25 @@ public interface DataAccessImplementor { */ ResultHandle findById(BytecodeCreator creator, ResultHandle id); + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page); + + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @param sort Sort instance that should be used in a query. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort); + /** * Find all entities. * diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java index 029513c3ed92d..ef1e2375ba6e8 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java @@ -34,6 +34,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { id); } + /** + * Implements Entity.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeStaticMethod(ofMethod(entityClassName, "findAll", PanacheQuery.class)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + + /** + * Implements Entity.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeStaticMethod( + ofMethod(entityClassName, "findAll", PanacheQuery.class, Sort.class), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + /** * Implements Entity.find(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java index f91f1a776508a..d8ff8b7bf81f3 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java @@ -39,6 +39,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { getRepositoryInstance(creator), id); } + /** + * Implements repository.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class), getRepositoryInstance(creator)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + + /** + * Implements repository.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class, Sort.class), + getRepositoryInstance(creator), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + /** * Implements repository.find(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java index 0d69dd8bacbbf..c7dbaaa8835ea 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java @@ -5,7 +5,9 @@ import java.util.List; import java.util.Map; +import javax.annotation.Priority; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; import javax.transaction.Transactional; import org.jboss.jandex.ClassInfo; @@ -55,11 +57,16 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem .build(); classCreator.addAnnotation(ApplicationScoped.class); + // The same resource is generated as part of the JaxRsResourceImplementor, so we need to avoid ambiguous resolution + // when injecting the resource in user beans: + classCreator.addAnnotation(Alternative.class); + classCreator.addAnnotation(Priority.class).add("value", Integer.MAX_VALUE); ResourceMethodListenerImplementor listenerImplementor = new ResourceMethodListenerImplementor(classCreator, resourceMethodListeners, false); implementList(classCreator, dataAccessImplementor); + implementListWithQuery(classCreator, dataAccessImplementor); implementListPageCount(classCreator, dataAccessImplementor); implementCount(classCreator, dataAccessImplementor); implementGet(classCreator, dataAccessImplementor); @@ -73,6 +80,20 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem } private void implementList(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { + MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class); + ResultHandle page = methodCreator.getMethodParam(0); + ResultHandle sort = methodCreator.getMethodParam(1); + ResultHandle columns = methodCreator.invokeVirtualMethod(ofMethod(Sort.class, "getColumns", List.class), sort); + ResultHandle isEmptySort = methodCreator.invokeInterfaceMethod(ofMethod(List.class, "isEmpty", boolean.class), columns); + + BranchResult isEmptySortBranch = methodCreator.ifTrue(isEmptySort); + isEmptySortBranch.trueBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.trueBranch(), page)); + isEmptySortBranch.falseBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.falseBranch(), page, sort)); + + methodCreator.close(); + } + + private void implementListWithQuery(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class, String.class, Map.class); ResultHandle page = methodCreator.getMethodParam(0); diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..b17e82af5ccb3 --- /dev/null +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java @@ -0,0 +1,17 @@ +package io.quarkus.hibernate.orm.rest.data.panache.deployment; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.contains; + +import org.junit.jupiter.api.Test; + +public abstract class AbstractInjectResourcesMethodTest { + + @Test + void shouldGetListOfItems() { + given().accept("application/json") + .when().get("/call/resource/items") + .then().statusCode(200) + .and().body("id", contains(1, 2)); + } +} diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java new file mode 100644 index 0000000000000..19f2e6e1e90fb --- /dev/null +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java @@ -0,0 +1,26 @@ +package io.quarkus.hibernate.orm.rest.data.panache.deployment.entity; + +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; + +@Path("/call/resource") +public class InjectionResource { + + @Inject + ItemsResource itemsResource; + + @GET + @Path("/items") + @Produces(MediaType.APPLICATION_JSON) + public List items() { + return itemsResource.list(new Page(5), Sort.by("id")); + } +} diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..7a70f6c533636 --- /dev/null +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.orm.rest.data.panache.deployment.entity; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.hibernate.orm.rest.data.panache.deployment.AbstractInjectResourcesMethodTest; +import io.quarkus.test.QuarkusUnitTest; + +class PanacheEntityResourceInjectResourcesMethodTest extends AbstractInjectResourcesMethodTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(PanacheEntityBase.class, PanacheEntity.class, Collection.class, CollectionsResource.class, + AbstractEntity.class, AbstractItem.class, Item.class, ItemsResource.class, InjectionResource.class) + .addAsResource("application.properties") + .addAsResource("import.sql")); +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java index 6d88361491536..9d3fec3a0b40a 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java @@ -17,6 +17,25 @@ public interface DataAccessImplementor { */ ResultHandle findById(BytecodeCreator creator, ResultHandle id); + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page); + + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @param sort Sort instance that should be used in a query. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort); + /** * Find all entities. * diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java index 95c5192b84525..ca0d84fbe5cff 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java @@ -34,6 +34,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { id); } + /** + * Implements Entity.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeStaticMethod(ofMethod(entityClassName, "findAll", PanacheQuery.class)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + + /** + * Implements Entity.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeStaticMethod( + ofMethod(entityClassName, "findAll", PanacheQuery.class, Sort.class), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + /** * Implements Entity.findAll(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java index 6382f63a11f9f..603ce94caec84 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java @@ -39,6 +39,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { getRepositoryInstance(creator), id); } + /** + * Implements repository.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class), getRepositoryInstance(creator)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + + /** + * Implements repository.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class, Sort.class), + getRepositoryInstance(creator), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + /** * Implements repository.find(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java index 448a741b85413..157a570140815 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java @@ -5,7 +5,9 @@ import java.util.List; import java.util.Map; +import javax.annotation.Priority; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.FieldInfo; @@ -56,13 +58,18 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem .build(); classCreator.addAnnotation(ApplicationScoped.class); + // The same resource is generated as part of the JaxRsResourceImplementor, so we need to avoid ambiguous resolution + // when injecting the resource in user beans: + classCreator.addAnnotation(Alternative.class); + classCreator.addAnnotation(Priority.class).add("value", Integer.MAX_VALUE); ResourceMethodListenerImplementor resourceMethodListenerImplementor = new ResourceMethodListenerImplementor( classCreator, resourceMethodListeners, true); implementList(classCreator, dataAccessImplementor); - implementCount(classCreator, dataAccessImplementor); + implementListWithQuery(classCreator, dataAccessImplementor); implementListPageCount(classCreator, dataAccessImplementor); + implementCount(classCreator, dataAccessImplementor); implementGet(classCreator, dataAccessImplementor); implementAdd(classCreator, dataAccessImplementor, resourceMethodListenerImplementor); implementUpdate(classCreator, dataAccessImplementor, entityType, resourceMethodListenerImplementor); @@ -74,6 +81,20 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem } private void implementList(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { + MethodCreator methodCreator = classCreator.getMethodCreator("list", Uni.class, Page.class, Sort.class); + ResultHandle page = methodCreator.getMethodParam(0); + ResultHandle sort = methodCreator.getMethodParam(1); + ResultHandle columns = methodCreator.invokeVirtualMethod(ofMethod(Sort.class, "getColumns", List.class), sort); + ResultHandle isEmptySort = methodCreator.invokeInterfaceMethod(ofMethod(List.class, "isEmpty", boolean.class), columns); + + BranchResult isEmptySortBranch = methodCreator.ifTrue(isEmptySort); + isEmptySortBranch.trueBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.trueBranch(), page)); + isEmptySortBranch.falseBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.falseBranch(), page, sort)); + + methodCreator.close(); + } + + private void implementListWithQuery(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("list", Uni.class, Page.class, Sort.class, String.class, Map.class); ResultHandle page = methodCreator.getMethodParam(0); diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..522d322d01d8b --- /dev/null +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java @@ -0,0 +1,17 @@ +package io.quarkus.hibernate.reactive.rest.data.panache.deployment; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.contains; + +import org.junit.jupiter.api.Test; + +public abstract class AbstractInjectResourcesMethodTest { + + @Test + void shouldGetListOfItems() { + given().accept("application/json") + .when().get("/call/resource/items") + .then().statusCode(200) + .and().body("id", contains(1, 2)); + } +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java new file mode 100644 index 0000000000000..19596484519c3 --- /dev/null +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java @@ -0,0 +1,27 @@ +package io.quarkus.hibernate.reactive.rest.data.panache.deployment.entity; + +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.smallrye.mutiny.Uni; + +@Path("/call/resource") +public class InjectionResource { + + @Inject + ItemsResource itemsResource; + + @GET + @Path("/items") + @Produces(MediaType.APPLICATION_JSON) + public Uni> items() { + return itemsResource.list(new Page(5), Sort.by("id")); + } +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..63615ecadc300 --- /dev/null +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.rest.data.panache.deployment.entity; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.panache.PanacheEntity; +import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; +import io.quarkus.hibernate.reactive.rest.data.panache.deployment.AbstractInjectResourcesMethodTest; +import io.quarkus.test.QuarkusUnitTest; + +class PanacheEntityResourceInjectResourcesMethodTest extends AbstractInjectResourcesMethodTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(PanacheEntityBase.class, PanacheEntity.class, Collection.class, CollectionsResource.class, + AbstractEntity.class, AbstractItem.class, Item.class, ItemsResource.class, InjectionResource.class) + .addAsResource("application.properties") + .addAsResource("import.sql")); +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties index ce8f669b3ab75..c58689900bbea 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties @@ -2,6 +2,6 @@ quarkus.datasource.db-kind=postgresql quarkus.datasource.username=hibernate_orm_test quarkus.datasource.password=hibernate_orm_test quarkus.datasource.reactive=true -quarkus.datasource.reactive.url=${postgres.reactive.url} +quarkus.datasource.reactive.url=vertx-reactive:postgresql://localhost:5431/hibernate_orm_test quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java index bbaa04cb04bc3..26662b646e1c2 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java @@ -4,6 +4,8 @@ import java.util.Arrays; import java.util.List; +import javax.annotation.Priority; +import javax.enterprise.inject.Alternative; import javax.inject.Inject; import javax.ws.rs.Path; @@ -78,6 +80,12 @@ void implement(ClassOutput classOutput, ResourceMetadata resourceMetadata, Resou } ClassCreator classCreator = classCreatorBuilder.build(); + // The same resource is generated as part of the ResourceImplementor, so we need to avoid ambiguous resolution + // when injecting the resource in user beans: + if (resourceMetadata.getResourceInterface() != null) { + classCreator.addAnnotation(Alternative.class); + classCreator.addAnnotation(Priority.class).add("value", Integer.MIN_VALUE); + } implementClassAnnotations(classCreator, resourceMetadata, resourceProperties, capabilities); FieldDescriptor resourceField = implementResourceField(classCreator, resourceMetadata); From f59253c610eac5ffaf3492d2f5f4804b6b33c36c Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 22 Dec 2022 08:39:10 +0200 Subject: [PATCH 03/17] Fix broken Feature#runtimeInitializedPackages method Relates to: https://github.com/quarkiverse/quarkus-neo4j/pull/115 (cherry picked from commit 6ac08e38e60bd46478808f1733a4749c37f98f9c) --- .../java/io/quarkus/deployment/steps/NativeImageFeatureStep.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java index 873cfce2d5abc..3e4e24105d90c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java @@ -282,6 +282,7 @@ public void write(String s, byte[] bytes) { CatchBlockCreator cc = tc.addCatch(Throwable.class); cc.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cc.getCaughtException()); } + runtimeInitializedPackages.returnValue(packagesArray); ResultHandle packages = overallCatch.invokeStaticMethod(runtimeInitializedPackages.getMethodDescriptor()); overallCatch.invokeStaticMethod(INITIALIZE_PACKAGES_AT_RUN_TIME, packages); From 310aa489424eff37069480565c87713cb3b2531e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Thu, 22 Dec 2022 03:04:57 +0100 Subject: [PATCH 04/17] Prevent repeated Quarkus Security exc. handling that duplicate headers fixes: #30011 (cherry picked from commit 101796c30f2061dafc5ce1d1da2cf0aedba71685) --- .../ResteasyStandaloneBuildStep.java | 54 ++++++++- ...thenticationFailedExceptionHeaderTest.java | 100 ++++++++++++++++ ...enticationRedirectExceptionHeaderTest.java | 109 ++++++++++++++++++ .../ResteasyStandaloneRecorder.java | 38 +++++- .../deployment/ResteasyReactiveProcessor.java | 42 ++++++- ...thenticationFailedExceptionHeaderTest.java | 91 +++++++++++++++ ...enticationRedirectExceptionHeaderTest.java | 100 ++++++++++++++++ .../runtime/ResteasyReactiveRecorder.java | 37 +++++- .../security/HttpSecurityRecorder.java | 3 + 9 files changed, 570 insertions(+), 4 deletions(-) create mode 100644 extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java create mode 100644 extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java index a78f85e87933d..a63334d0cbdf1 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java @@ -5,6 +5,12 @@ import java.util.Optional; +import javax.ws.rs.ext.ExceptionMapper; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Type; + import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -14,19 +20,27 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExecutorBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.resteasy.common.deployment.ResteasyInjectionReadyBuildItem; +import io.quarkus.resteasy.runtime.AuthenticationCompletionExceptionMapper; +import io.quarkus.resteasy.runtime.AuthenticationFailedExceptionMapper; +import io.quarkus.resteasy.runtime.AuthenticationRedirectExceptionMapper; import io.quarkus.resteasy.runtime.ResteasyVertxConfig; import io.quarkus.resteasy.runtime.standalone.ResteasyStandaloneRecorder; import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentBuildItem; +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.DefaultRouteBuildItem; import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -34,6 +48,7 @@ public class ResteasyStandaloneBuildStep { private static final int REST_ROUTE_ORDER_OFFSET = 500; + private static final DotName EXCEPTION_MAPPER = DotName.createSimple(ExceptionMapper.class.getName()); public static final class ResteasyStandaloneBuildItem extends SimpleBuildItem { @@ -75,6 +90,8 @@ public void boot(ShutdownContextBuildItem shutdown, BuildProducer routes, BuildProducer filterBuildItemBuildProducer, CoreVertxBuildItem vertx, + CombinedIndexBuildItem combinedIndexBuildItem, + HttpBuildTimeConfig vertxConfig, ResteasyStandaloneBuildItem standalone, Optional requireVirtual, ExecutorBuildItem executorBuildItem, @@ -90,11 +107,28 @@ public void boot(ShutdownContextBuildItem shutdown, Handler handler = recorder.vertxRequestHandler(vertx.getVertx(), executorBuildItem.getExecutorProxy(), resteasyVertxConfig); + final boolean noCustomAuthCompletionExMapper; + final boolean noCustomAuthFailureExMapper; + final boolean noCustomAuthRedirectExMapper; + if (vertxConfig.auth.proactive) { + noCustomAuthCompletionExMapper = notFoundCustomExMapper(AuthenticationCompletionException.class.getName(), + AuthenticationCompletionExceptionMapper.class.getName(), combinedIndexBuildItem.getIndex()); + noCustomAuthFailureExMapper = notFoundCustomExMapper(AuthenticationFailedException.class.getName(), + AuthenticationFailedExceptionMapper.class.getName(), combinedIndexBuildItem.getIndex()); + noCustomAuthRedirectExMapper = notFoundCustomExMapper(AuthenticationRedirectException.class.getName(), + AuthenticationRedirectExceptionMapper.class.getName(), combinedIndexBuildItem.getIndex()); + } else { + // with disabled proactive auth we need to handle exceptions anyway as default auth failure handler did not + noCustomAuthCompletionExMapper = false; + noCustomAuthFailureExMapper = false; + noCustomAuthRedirectExMapper = false; + } // failure handler for auth failures that occurred before the handler defined right above started processing the request // we add the failure handler right before QuarkusErrorHandler // so that user can define failure handlers that precede exception mappers final Handler failureHandler = recorder.vertxFailureHandler(vertx.getVertx(), - executorBuildItem.getExecutorProxy(), resteasyVertxConfig); + executorBuildItem.getExecutorProxy(), resteasyVertxConfig, noCustomAuthCompletionExMapper, + noCustomAuthFailureExMapper, noCustomAuthRedirectExMapper, vertxConfig.auth.proactive); filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path @@ -117,6 +151,24 @@ public void boot(ShutdownContextBuildItem shutdown, recorder.start(shutdown, requireVirtual.isPresent()); } + private static boolean notFoundCustomExMapper(String exSignatureStr, String exMapperSignatureStr, IndexView index) { + for (var implementor : index.getAllKnownImplementors(EXCEPTION_MAPPER)) { + if (exMapperSignatureStr.equals(implementor.name().toString())) { + continue; + } + for (Type interfaceType : implementor.interfaceTypes()) { + if (EXCEPTION_MAPPER.equals(interfaceType.name())) { + final String mapperExSignature = interfaceType.asParameterizedType().arguments().get(0).name().toString(); + if (exSignatureStr.equals(mapperExSignature)) { + return false; + } + break; + } + } + } + return true; + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT) public FilterBuildItem addDefaultAuthFailureHandler(ResteasyStandaloneRecorder recorder) { diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java new file mode 100644 index 0000000000000..740494074d9d6 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java @@ -0,0 +1,100 @@ +package io.quarkus.resteasy.test.security; + +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationFailedExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that there is only one location header + // there has been duplicate location when both default auth failure handler and auth ex mapper send challenge + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(302); + assertEquals(1, response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(LOCATION.toString()::equals).count()); + } + + @Path("/hello") + public static class HelloResource { + @GET + public String hello() { + return "hello"; + } + } + + @ApplicationScoped + public static class FailingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(302, LOCATION, "http://localhost:8080/")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java new file mode 100644 index 0000000000000..b46efe9d16aef --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java @@ -0,0 +1,109 @@ +package io.quarkus.resteasy.test.security; + +import static io.vertx.core.http.HttpHeaders.CACHE_CONTROL; +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.restassured.response.Response; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationRedirectExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that Pragma, cache-control and location headers are only present once + // there were duplicate headers when both default auth failure handler and auth ex mapper set headers + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(302); + assertEquals(1, getHeaderCount(response, LOCATION.toString())); + assertEquals(1, getHeaderCount(response, CACHE_CONTROL.toString())); + assertEquals(1, getHeaderCount(response, "Pragma")); + } + + private static int getHeaderCount(Response response, String headerName) { + headerName = headerName.toLowerCase(); + return (int) response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(headerName::equals).count(); + } + + @Path("/hello") + public static class HelloResource { + @GET + public String hello() { + return "hello"; + } + } + + @ApplicationScoped + public static class RedirectingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationRedirectException(302, "https://quarkus.io/")); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(302, "header-name", "header-value")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java index 253be4d73b8ab..9a290ac4a2c11 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java @@ -78,7 +78,9 @@ public Handler vertxRequestHandler(Supplier vertx, Execut return null; } - public Handler vertxFailureHandler(Supplier vertx, Executor executor, ResteasyVertxConfig config) { + public Handler vertxFailureHandler(Supplier vertx, Executor executor, ResteasyVertxConfig config, + boolean noCustomAuthCompletionExMapper, boolean noCustomAuthFailureExMapper, boolean noCustomAuthRedirectExMapper, + boolean proactive) { if (deployment == null) { return null; } else { @@ -90,6 +92,40 @@ public Handler vertxFailureHandler(Supplier vertx, Execut @Override public void handle(RoutingContext request) { + + // special handling when proactive auth is enabled as then we know default auth failure handler already run + if (proactive && request.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof DefaultAuthFailureHandler) { + // we want to prevent repeated handling of exceptions if user don't want to handle exception himself + // we do not pass exception to abort handlers if proactive auth is enabled and user did not + // provide custom ex. mapper; we replace default auth failure handler as soon as we can, so that + // we can handle Quarkus Security Exceptions ourselves + if (request.failure() instanceof AuthenticationFailedException) { + if (noCustomAuthFailureExMapper) { + request.next(); + } else { + // allow response customization + super.handle(request); + } + return; + } else if (request.failure() instanceof AuthenticationCompletionException) { + if (noCustomAuthCompletionExMapper) { + request.next(); + } else { + // allow response customization + super.handle(request); + } + return; + } else if (request.failure() instanceof AuthenticationRedirectException) { + if (noCustomAuthRedirectExMapper) { + request.next(); + } else { + // allow response customization + super.handle(request); + } + return; + } + } + if (request.failure() instanceof AuthenticationFailedException || request.failure() instanceof AuthenticationCompletionException || request.failure() instanceof AuthenticationRedirectException diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 92dfa45304f3e..688b05dc843a9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -188,6 +188,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.security.ForbiddenException; import io.quarkus.vertx.http.deployment.FilterBuildItem; @@ -1203,7 +1204,26 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, if (!requestContextFactoryBuildItem.isPresent()) { RuntimeValue restInitialHandler = recorder.restInitialHandler(deployment); Handler handler = recorder.handler(restInitialHandler); - Handler failureHandler = recorder.failureHandler(restInitialHandler); + + final boolean noCustomAuthCompletionExMapper; + final boolean noCustomAuthFailureExMapper; + final boolean noCustomAuthRedirectExMapper; + if (vertxConfig.auth.proactive) { + noCustomAuthCompletionExMapper = notFoundCustomExMapper(AuthenticationCompletionException.class.getName(), + AuthenticationCompletionExceptionMapper.class.getName(), exceptionMapping); + noCustomAuthFailureExMapper = notFoundCustomExMapper(AuthenticationFailedException.class.getName(), + AuthenticationFailedExceptionMapper.class.getName(), exceptionMapping); + noCustomAuthRedirectExMapper = notFoundCustomExMapper(AuthenticationRedirectException.class.getName(), + AuthenticationRedirectExceptionMapper.class.getName(), exceptionMapping); + } else { + // with disabled proactive auth we need to handle exceptions anyway as default auth failure handler did not + noCustomAuthCompletionExMapper = false; + noCustomAuthFailureExMapper = false; + noCustomAuthRedirectExMapper = false; + } + + Handler failureHandler = recorder.failureHandler(restInitialHandler, noCustomAuthCompletionExMapper, + noCustomAuthFailureExMapper, noCustomAuthRedirectExMapper, vertxConfig.auth.proactive); // we add failure handler right before QuarkusErrorHandler // so that user can define failure handlers that precede exception mappers @@ -1225,6 +1245,26 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, } } + private static boolean notFoundCustomExMapper(String builtInExSignature, String builtInMapperSignature, + ExceptionMapping exceptionMapping) { + for (var entry : exceptionMapping.getMappers().entrySet()) { + if (builtInExSignature.equals(entry.getKey()) + && !entry.getValue().getClassName().startsWith(builtInMapperSignature)) { + return false; + } + } + for (var entry : exceptionMapping.getRuntimeCheckMappers().entrySet()) { + if (builtInExSignature.equals(entry.getKey())) { + for (var resourceExceptionMapper : entry.getValue()) { + if (!resourceExceptionMapper.getClassName().startsWith(builtInMapperSignature)) { + return false; + } + } + } + } + return true; + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT) public FilterBuildItem addDefaultAuthFailureHandler(ResteasyReactiveRecorder recorder) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java new file mode 100644 index 0000000000000..4b4c3d67efe79 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java @@ -0,0 +1,91 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationFailedExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that there is only one location header + // there has been duplicate location when both default auth failure handler and auth ex mapper send challenge + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(FOUND); + assertEquals(1, response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(LOCATION.toString()::equals).count()); + } + + @ApplicationScoped + public static class FailingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(FOUND, LOCATION, "http://localhost:8080/")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java new file mode 100644 index 0000000000000..35157e762ad77 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java @@ -0,0 +1,100 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.vertx.core.http.HttpHeaders.CACHE_CONTROL; +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.restassured.response.Response; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationRedirectExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that Pragma, cache-control and location headers are only present once + // there were duplicate headers when both default auth failure handler and auth ex mapper set headers + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(FOUND); + assertEquals(1, getHeaderCount(response, LOCATION.toString())); + assertEquals(1, getHeaderCount(response, CACHE_CONTROL.toString())); + assertEquals(1, getHeaderCount(response, "Pragma")); + } + + private static int getHeaderCount(Response response, String headerName) { + headerName = headerName.toLowerCase(); + return (int) response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(headerName::equals).count(); + } + + @ApplicationScoped + public static class RedirectingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationRedirectException(FOUND, "https://quarkus.io/")); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(FOUND, "header-name", "header-value")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 6cc91a1899e50..49eaf3c7159ee 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -212,13 +212,48 @@ public void accept(RoutingContext routingContext) { return new ResteasyReactiveVertxHandler(eventCustomizer, initialHandler); } - public Handler failureHandler(RuntimeValue restInitialHandlerRuntimeValue) { + public Handler failureHandler(RuntimeValue restInitialHandlerRuntimeValue, + boolean noCustomAuthCompletionExMapper, boolean noCustomAuthFailureExMapper, boolean noCustomAuthRedirectExMapper, + boolean proactive) { final RestInitialHandler restInitialHandler = restInitialHandlerRuntimeValue.getValue(); // process auth failures with abort handlers return new Handler() { @Override public void handle(RoutingContext event) { + // special handling when proactive auth is enabled as then we know default auth failure handler already run + if (proactive && event.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof DefaultAuthFailureHandler) { + // we want to prevent repeated handling of exceptions if user don't want to handle exception himself + // we do not pass exception to abort handlers if proactive auth is enabled and user did not + // provide custom ex. mapper; we replace default auth failure handler as soon as we can, so that + // we can handle Quarkus Security Exceptions ourselves + if (event.failure() instanceof AuthenticationFailedException) { + if (noCustomAuthFailureExMapper) { + event.next(); + } else { + // allow response customization + restInitialHandler.beginProcessing(event, event.failure()); + } + return; + } else if (event.failure() instanceof AuthenticationCompletionException) { + if (noCustomAuthCompletionExMapper) { + event.next(); + } else { + // allow response customization + restInitialHandler.beginProcessing(event, event.failure()); + } + return; + } else if (event.failure() instanceof AuthenticationRedirectException) { + if (noCustomAuthRedirectExMapper) { + event.next(); + } else { + // allow response customization + restInitialHandler.beginProcessing(event, event.failure()); + } + return; + } + } + if (event.failure() instanceof AuthenticationFailedException || event.failure() instanceof AuthenticationCompletionException || event.failure() instanceof AuthenticationRedirectException diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index ffc0fff14ceee..5505c4eb0c133 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -310,6 +310,9 @@ protected DefaultAuthFailureHandler() { @Override public void accept(RoutingContext event, Throwable throwable) { + if (event.response().ended()) { + return; + } throwable = extractRootCause(throwable); //auth failed if (throwable instanceof AuthenticationFailedException) { From b9ec03ba73d8f1effa15fe74724de8fa5d777c89 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 22 Dec 2022 17:06:49 +0100 Subject: [PATCH 05/17] Do not close the managed ValidatorFactory When creating ValidatorFactory manually, we now returns the Quarkus-managed instance. We need to be careful not to close it, thus why we now wrap it to make close() a noop. (cherry picked from commit d8460d43b9efd045c11bfbe0bf0ead98fc754c69) --- .../ValidatorFactoryFromValidationTest.java | 6 +- .../CloseAsNoopValidatorFactoryWrapper.java | 93 +++++++++++++++++++ .../runtime/HibernateValidatorRecorder.java | 3 +- .../validator/runtime/ValidationSupport.java | 7 +- .../validator/runtime/ValidatorHolder.java | 9 +- .../validator/runtime/ValidatorProvider.java | 5 +- 6 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java index 0211354d0c6fe..224379c236e97 100644 --- a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java @@ -6,6 +6,7 @@ import javax.validation.Validation; import javax.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidatorFactory; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; @@ -23,9 +24,10 @@ public class ValidatorFactoryFromValidationTest { .create(JavaArchive.class)); @Test - public void testOverrideConstraintValidatorConstraint() { + public void testValidatorFactoryManuallyCreatedIsManagedByQuarkus() { ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); - assertThat(validatorFactoryFromInjection).isSameAs(validatorFactory); + // we need to unwrap here as we have a small wrapper around the manually created one + assertThat(validatorFactoryFromInjection).isSameAs(validatorFactory.unwrap(HibernateValidatorFactory.class)); } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java new file mode 100644 index 0000000000000..600fb72a6dcd1 --- /dev/null +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java @@ -0,0 +1,93 @@ +package io.quarkus.hibernate.validator.runtime; + +import java.time.Duration; + +import javax.validation.ClockProvider; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.MessageInterpolator; +import javax.validation.ParameterNameProvider; +import javax.validation.TraversableResolver; +import javax.validation.Validator; + +import org.hibernate.validator.HibernateValidatorContext; +import org.hibernate.validator.HibernateValidatorFactory; +import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider; +import org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy; +import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory; + +/** + * Wrapper used to avoid closing the managed ValidatorFactory. + */ +class CloseAsNoopValidatorFactoryWrapper implements HibernateValidatorFactory { + + private final HibernateValidatorFactory validatorFactory; + + CloseAsNoopValidatorFactoryWrapper(HibernateValidatorFactory validatorFactory) { + this.validatorFactory = validatorFactory; + } + + @Override + public void close() { + // do not close the wrapped ValidatorFactory as it is managed by Quarkus + } + + @Override + public Validator getValidator() { + return validatorFactory.getValidator(); + } + + @Override + public MessageInterpolator getMessageInterpolator() { + return validatorFactory.getMessageInterpolator(); + } + + @Override + public TraversableResolver getTraversableResolver() { + return validatorFactory.getTraversableResolver(); + } + + @Override + public ConstraintValidatorFactory getConstraintValidatorFactory() { + return validatorFactory.getConstraintValidatorFactory(); + } + + @Override + public ParameterNameProvider getParameterNameProvider() { + return validatorFactory.getParameterNameProvider(); + } + + @Override + public ClockProvider getClockProvider() { + return validatorFactory.getClockProvider(); + } + + @Override + public T unwrap(Class type) { + return validatorFactory.unwrap(type); + } + + @Override + public ScriptEvaluatorFactory getScriptEvaluatorFactory() { + return validatorFactory.getScriptEvaluatorFactory(); + } + + @Override + public Duration getTemporalValidationTolerance() { + return validatorFactory.getTemporalValidationTolerance(); + } + + @Override + public GetterPropertySelectionStrategy getGetterPropertySelectionStrategy() { + return validatorFactory.getGetterPropertySelectionStrategy(); + } + + @Override + public PropertyNodeNameProvider getPropertyNodeNameProvider() { + return validatorFactory.getPropertyNodeNameProvider(); + } + + @Override + public HibernateValidatorContext usingContext() { + return validatorFactory.usingContext(); + } +} 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 385e16d0c5fe9..d6e6b1725e1e8 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 @@ -14,6 +14,7 @@ import javax.validation.ValidatorFactory; import javax.validation.valueextraction.ValueExtractor; +import org.hibernate.validator.HibernateValidatorFactory; import org.hibernate.validator.PredefinedScopeHibernateValidator; import org.hibernate.validator.PredefinedScopeHibernateValidatorConfiguration; import org.hibernate.validator.internal.engine.resolver.JPATraversableResolver; @@ -160,7 +161,7 @@ public void created(BeanContainer container) { } ValidatorFactory validatorFactory = configuration.buildValidatorFactory(); - ValidatorHolder.initialize(validatorFactory); + ValidatorHolder.initialize(validatorFactory.unwrap(HibernateValidatorFactory.class)); // Close the ValidatorFactory on shutdown shutdownContext.addShutdownTask(new Runnable() { diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java index bfebed445ea47..444defaf83fd9 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java @@ -3,6 +3,8 @@ import javax.validation.Validation; import javax.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidatorFactory; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; @@ -12,19 +14,18 @@ public final class ValidationSupport { private ValidationSupport() { } - @SuppressWarnings("unused") public static ValidatorFactory buildDefaultValidatorFactory() { ArcContainer container = Arc.container(); if (container == null) { return fallback(); } - InstanceHandle instance = container.instance(ValidatorFactory.class); + InstanceHandle instance = container.instance(HibernateValidatorFactory.class); if (!instance.isAvailable()) { return fallback(); } - return instance.get(); + return new CloseAsNoopValidatorFactoryWrapper(instance.get()); } // the point of having this is to support non-Quarkus tests that could be using Hibernate Validator diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java index 5b4d3bb95cfc0..e039659986499 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java @@ -1,20 +1,21 @@ package io.quarkus.hibernate.validator.runtime; import javax.validation.Validator; -import javax.validation.ValidatorFactory; + +import org.hibernate.validator.HibernateValidatorFactory; public class ValidatorHolder { - private static ValidatorFactory validatorFactory; + private static HibernateValidatorFactory validatorFactory; private static Validator validator; - static void initialize(ValidatorFactory validatorFactory) { + static void initialize(HibernateValidatorFactory validatorFactory) { ValidatorHolder.validatorFactory = validatorFactory; ValidatorHolder.validator = validatorFactory.getValidator(); } - static ValidatorFactory getValidatorFactory() { + static HibernateValidatorFactory getValidatorFactory() { return validatorFactory; } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java index e4040a04e21f8..06af13698972e 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java @@ -4,7 +4,8 @@ import javax.inject.Named; import javax.inject.Singleton; import javax.validation.Validator; -import javax.validation.ValidatorFactory; + +import org.hibernate.validator.HibernateValidatorFactory; @Singleton public class ValidatorProvider { @@ -12,7 +13,7 @@ public class ValidatorProvider { @Produces @Named("quarkus-hibernate-validator-factory") @Singleton - public ValidatorFactory factory() { + public HibernateValidatorFactory factory() { return ValidatorHolder.getValidatorFactory(); } From a3b6a45b6ababf855cbe55c57126610d1211b3a4 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Thu, 22 Dec 2022 14:05:41 +0100 Subject: [PATCH 06/17] Updates Infinispan dependencies * Infinispan 14.0.4.Final * Protostream 4.5.1.Final (cherry picked from commit 1a6d40d91d889ddce74627f53d048d1cfe32203f) --- bom/application/pom.xml | 4 ++-- docs/src/main/asciidoc/infinispan-client.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 67ed6c13d5953..b091fa8562c8f 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -131,8 +131,8 @@ 5.9.1 1.5.0 6.14.2 - 14.0.3.Final - 4.5.0.Final + 14.0.4.Final + 4.5.1.Final 3.1.1 4.1.86.Final 1.8.0 diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc index 75e343843d1b0..a09fb6f755a2d 100644 --- a/docs/src/main/asciidoc/infinispan-client.adoc +++ b/docs/src/main/asciidoc/infinispan-client.adoc @@ -44,9 +44,9 @@ This command adds the following dependency to your build file: .build.gradle ---- implementation 'io.quarkus:quarkus-infinispan-client' -annotationProcessor 'org.infinispan.protostream:protostream-processor:4.5.0.Final' <1> +annotationProcessor 'org.infinispan.protostream:protostream-processor:4.5.1.Final' <1> ---- -<1> Mandatory in the gradle build to enable the generation of the files in the annotation based serialization +<1> Mandatory in the Gradle build to enable the generation of the files in the annotation based serialization == Configuring the Infinispan client From fb45292e026a860daae54d9bc75ad0dc12a02eaf Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 20 Dec 2022 16:44:08 +0100 Subject: [PATCH 07/17] Pass missing properties to Hibernate Reactive startup Fixes #29956 (cherry picked from commit aab1831220bc82daab9447948b23fa9a5f77f0ba) --- .../reactive/deployment/HibernateReactiveProcessor.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java index c5f5207fa3be5..0fdcd1e39abea 100644 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java +++ b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java @@ -310,6 +310,13 @@ private static ParsedPersistenceXmlDescriptor generateReactivePersistenceUnit( String.valueOf(persistenceUnitConfig.query.inClauseParameterPadding)); // JDBC + persistenceUnitConfig.jdbc.timezone.ifPresent( + timezone -> desc.getProperties().setProperty(AvailableSettings.JDBC_TIME_ZONE, timezone)); + + persistenceUnitConfig.jdbc.statementFetchSize.ifPresent( + fetchSize -> desc.getProperties().setProperty(AvailableSettings.STATEMENT_FETCH_SIZE, + String.valueOf(fetchSize))); + persistenceUnitConfig.jdbc.statementBatchSize.ifPresent( statementBatchSize -> desc.getProperties().setProperty(AvailableSettings.STATEMENT_BATCH_SIZE, String.valueOf(statementBatchSize))); From 914c623e25d9a7e9a66f478226642fa2bb9ffea9 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 23 Dec 2022 14:52:29 +0200 Subject: [PATCH 08/17] Ensure that Kotlin subclass of QuarkusApplication works properly Fixes: #30054 (cherry picked from commit 59e73b7a235b5f7ac86b36d1ac17a685f090a804) --- .../deployment/steps/MainClassBuildStep.java | 35 ++++++++++++++----- .../src/main/kotlin/org/acme/MyMainClass.kt | 13 +++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/MyMainClass.kt diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index cfefd34f70fa4..5238f2e607e2f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -21,6 +21,7 @@ import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -102,6 +103,8 @@ public class MainClassBuildStep { void.class); public static final MethodDescriptor CONFIGURE_STEP_TIME_START = ofMethod(StepTiming.class.getName(), "configureStart", void.class); + private static final DotName QUARKUS_APPLICATION = DotName.createSimple(QuarkusApplication.class.getName()); + private static final DotName OBJECT = DotName.createSimple(Object.class.getName()); @BuildStep void build(List staticInitTasks, @@ -335,7 +338,8 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer quarkusMainAnnotations = new HashMap<>(); - Collection quarkusMains = combinedIndexBuildItem.getIndex() + IndexView index = combinedIndexBuildItem.getIndex(); + Collection quarkusMains = index .getAnnotations(DotName.createSimple(QuarkusMain.class.getName())); for (AnnotationInstance i : quarkusMains) { AnnotationValue nameValue = i.value("name"); @@ -349,7 +353,7 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer impls = combinedIndexBuildItem.getIndex() - .getAllKnownImplementors(DotName.createSimple(QuarkusApplication.class.getName())); - ClassInfo classByName = combinedIndexBuildItem.getIndex().getClassByName(DotName.createSimple(mainClassName)); + Collection impls = index + .getAllKnownImplementors(QUARKUS_APPLICATION); + ClassInfo classByName = index.getClassByName(DotName.createSimple(mainClassName)); MethodInfo mainClassMethod = null; if (classByName != null) { mainClassMethod = classByName @@ -401,7 +405,7 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer Date: Mon, 26 Dec 2022 11:16:02 +1100 Subject: [PATCH 09/17] Update smallrye-health.adoc Minor singular / plural fixes (cherry picked from commit 225a5f9ab0109dc2a43c8cc96baca0271d6e5481) --- docs/src/main/asciidoc/smallrye-health.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/smallrye-health.adoc b/docs/src/main/asciidoc/smallrye-health.adoc index 3c85c82dd6067..a0e0039e69fc5 100644 --- a/docs/src/main/asciidoc/smallrye-health.adoc +++ b/docs/src/main/asciidoc/smallrye-health.adoc @@ -393,9 +393,9 @@ public class LivenessAsync implements AsyncHealthCheck { Some extension may provide default health checks, including the extension will automatically register its health checks. For example, `quarkus-agroal` that is used to manage Quarkus datasource(s) automatically register a readiness health check -that will validate each datasources: xref:datasource.adoc#datasource-health-check[Datasource Health Check]. +that will validate each datasource: xref:datasource.adoc#datasource-health-check[Datasource Health Check]. -You can disable extension health check via the property `quarkus.health.extensions.enabled` so none will be automatically registered. +You can disable extension health checks via the property `quarkus.health.extensions.enabled` so none will be automatically registered. [[ui]] == Health UI From 651e71084cd39d943be9fea898d6eb514da854ac Mon Sep 17 00:00:00 2001 From: Gwenneg Lepage Date: Mon, 2 Jan 2023 11:35:13 +0100 Subject: [PATCH 10/17] Change quarkus-cache extension status to stable (cherry picked from commit 9413966ac1fe8b32ae60940c1e2b3dcfc3841d41) --- .../runtime/src/main/resources/META-INF/quarkus-extension.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 186ae789f8fa1..24b7c04af0ad8 100644 --- a/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,6 +8,6 @@ metadata: guide: "https://quarkus.io/guides/cache" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.cache." From b84956fefb093acf00297d8e573115a74531d0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Mon, 2 Jan 2023 16:48:45 +0100 Subject: [PATCH 11/17] Update the performance guide with respect to Quarkus default GraalVM GC (cherry picked from commit 43157cbfd8f162a174c2f809af9a59f0d44fd15b) --- docs/src/main/asciidoc/performance-measure.adoc | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/src/main/asciidoc/performance-measure.adoc b/docs/src/main/asciidoc/performance-measure.adoc index cd261ce68ef95..51ca1c4b1e1b9 100644 --- a/docs/src/main/asciidoc/performance-measure.adoc +++ b/docs/src/main/asciidoc/performance-measure.adoc @@ -224,13 +224,6 @@ all services it's able to find on the classpath. We prefer listing services explicitly as it produces better optimised binaries. Disable it as well by setting `-H:-UseServiceLoaderFeature`. -=== Better default for Garbage Collection implementation - -The default in GraalVM seems meant to optimise for short-lived processes. - -Quarkus defaults to server applications, so we switch to a better default by setting - `-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime`. - === Others ... This section is provided as high level guidance, but can't presume to be comprehensive as some flags are controlled From 1499d1c06ad0a3838c12074b73b2be30c980bb1f Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 3 Jan 2023 11:44:10 +0100 Subject: [PATCH 12/17] Hibernate Validator - Normalize ClientProxy for bean metadata retrieval (cherry picked from commit b9969efaaf7c3369822456c80c52dc293ab1b6ea) --- .../runtime/ArcProxyBeanMetaDataClassNormalizer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java index 674b387ad6d37..03d66360d2db5 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java @@ -2,12 +2,13 @@ import org.hibernate.validator.metadata.BeanMetaDataClassNormalizer; +import io.quarkus.arc.ClientProxy; import io.quarkus.arc.Subclass; /** * In the case of a proxy generated by Arc, return the parent class. *
- * This can have more than one level of hierarchy, in example: + * This can have more than one level of hierarchy, for example: *
    *
  • When using @{@link io.quarkus.test.junit.mockito.InjectMock} or @{@link io.quarkus.test.junit.mockito.InjectSpy}
  • *
@@ -20,6 +21,9 @@ public Class normalize(Class beanClass) { while (Subclass.class.isAssignableFrom(targetClass)) { targetClass = targetClass.getSuperclass(); } + while (ClientProxy.class.isAssignableFrom(targetClass)) { + targetClass = targetClass.getSuperclass(); + } return targetClass; } From b6eeaa0290dd71d6424b2772ed887253a32fcfed Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Tue, 3 Jan 2023 12:18:38 +0100 Subject: [PATCH 13/17] Make sure Federation annotations are in the index, add a test (cherry picked from commit 7d129dd72c27954eb5974d08b4fb0c642f2f5e34) --- .../deployment/SmallRyeGraphQLProcessor.java | 10 ++++ .../deployment/GraphQLFederationTest.java | 59 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java 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 e6e11bc9419e8..0ac3da55c1ebd 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 @@ -71,6 +71,11 @@ import io.smallrye.graphql.api.AdaptWith; import io.smallrye.graphql.api.Entry; import io.smallrye.graphql.api.ErrorExtensionProvider; +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.External; +import io.smallrye.graphql.api.federation.Key; +import io.smallrye.graphql.api.federation.Provides; +import io.smallrye.graphql.api.federation.Requires; import io.smallrye.graphql.cdi.config.ConfigKey; import io.smallrye.graphql.cdi.config.MicroProfileConfig; import io.smallrye.graphql.cdi.producer.GraphQLProducer; @@ -244,6 +249,11 @@ void buildFinalIndex( try { indexer.indexClass(Map.class); indexer.indexClass(Entry.class); + indexer.indexClass(Extends.class); + indexer.indexClass(External.class); + indexer.indexClass(Key.class); + indexer.indexClass(Provides.class); + indexer.indexClass(Requires.class); } catch (IOException ex) { LOG.warn("Failure while creating index", ex); } diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java new file mode 100644 index 0000000000000..587f597fea5a8 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java @@ -0,0 +1,59 @@ +package io.quarkus.smallrye.graphql.deployment; + +import static org.hamcrest.Matchers.containsString; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.graphql.api.federation.Extends; + +public class GraphQLFederationTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(FooApi.class, Foo.class) + .addAsResource(new StringAsset("quarkus.smallrye-graphql.schema-include-directives=true"), + "application.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void checkServiceDeclarationInSchema() { + RestAssured.given() + .get("/graphql/schema.graphql") + .then() + .body(containsString("type _Service {")); + } + + @Test + public void checkFederationDirectivesInSchema() { + RestAssured.given() + .get("/graphql/schema.graphql") + .then() + .body(containsString("name: String @extends")); + } + + @GraphQLApi + static class FooApi { + + @Query + public Foo foo() { + return new Foo(); + } + + } + + static class Foo { + + @Extends + public String name; + + } + +} From 81edd04488c95793ad0743828f6fb4d6d26adbd0 Mon Sep 17 00:00:00 2001 From: ozangunalp Date: Thu, 22 Dec 2022 11:12:39 +0000 Subject: [PATCH 14/17] Kafka ui RPC creates own ObjectMapper (deserialization) Fixes #29976 (cherry picked from commit b2ac6b2d01c995836bc9be8d4ee2801ab5f53742) --- .../client/runtime/ui/KafkaUiHandler.java | 12 +++++------ .../kafka/client/runtime/ui/KafkaUiUtils.java | 21 ++++++++++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java index 8b7d916de765b..06aea79b5b9f5 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java @@ -24,7 +24,8 @@ public void handlePost(RoutingContext event) { endResponse(event, BAD_REQUEST, "Request body is null"); return; } - var body = event.body().asJsonObject(); + var webUtils = kafkaWebUiUtils(); + var body = webUtils.fromJson(event.body().buffer()); if (body == null) { endResponse(event, BAD_REQUEST, "Request JSON body is null"); return; @@ -34,7 +35,6 @@ public void handlePost(RoutingContext event) { var message = "OK"; var error = ""; - var webUtils = kafkaWebUiUtils(); var adminClient = kafkaAdminClient(); boolean res = false; @@ -50,7 +50,7 @@ public void handlePost(RoutingContext event) { res = true; break; case "createTopic": - var topicCreateRq = event.body().asPojo(KafkaCreateTopicRequest.class); + var topicCreateRq = webUtils.fromJson(event.body().buffer(), KafkaCreateTopicRequest.class); res = adminClient.createTopic(topicCreateRq); message = webUtils.toJson(webUtils.getTopics()); break; @@ -64,17 +64,17 @@ public void handlePost(RoutingContext event) { res = true; break; case "topicMessages": - var msgRequest = event.body().asPojo(KafkaMessagesRequest.class); + var msgRequest = webUtils.fromJson(event.body().buffer(), KafkaMessagesRequest.class); message = webUtils.toJson(webUtils.getMessages(msgRequest)); res = true; break; case "getOffset": - var request = event.body().asPojo(KafkaOffsetRequest.class); + var request = webUtils.fromJson(event.body().buffer(), KafkaOffsetRequest.class); message = webUtils.toJson(webUtils.getOffset(request)); res = true; break; case "createMessage": - var rq = event.body().asPojo(KafkaMessageCreateRequest.class); + var rq = webUtils.fromJson(event.body().buffer(), KafkaMessageCreateRequest.class); webUtils.createMessage(rq); message = "{}"; res = true; diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java index bc9b3ba525dbd..6cca91a20f37e 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java @@ -2,6 +2,8 @@ import static io.quarkus.kafka.client.runtime.ui.util.ConsumerFactory.createConsumer; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -21,12 +23,14 @@ import org.apache.kafka.common.Node; import org.apache.kafka.common.TopicPartition; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; +import io.netty.buffer.ByteBufInputStream; import io.quarkus.kafka.client.runtime.KafkaAdminClient; import io.quarkus.kafka.client.runtime.ui.model.Order; import io.quarkus.kafka.client.runtime.ui.model.request.KafkaMessageCreateRequest; @@ -34,6 +38,8 @@ import io.quarkus.kafka.client.runtime.ui.model.request.KafkaOffsetRequest; import io.quarkus.kafka.client.runtime.ui.model.response.*; import io.smallrye.common.annotation.Identifier; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; @Singleton public class KafkaUiUtils { @@ -229,9 +235,22 @@ public String toJson(Object o) { try { res = objectMapper.writeValueAsString(o); } catch (JsonProcessingException ex) { - //FIXME: res = ""; } return res; } + + public JsonObject fromJson(Buffer buffer) { + return new JsonObject(fromJson(buffer, Map.class)); + } + + public T fromJson(Buffer buffer, Class type) { + try { + JsonParser parser = objectMapper.createParser((InputStream) new ByteBufInputStream(buffer.getByteBuf())); + return objectMapper.readValue(parser, type); + } catch (IOException e) { + return null; + } + } + } From 1b2643097afade930101791a4f38e781ede5a3af Mon Sep 17 00:00:00 2001 From: ozangunalp Date: Thu, 22 Dec 2022 11:13:36 +0000 Subject: [PATCH 15/17] Calculate Kafka dev ui RPC path from current window, for custom http root path Fixes #30029 (cherry picked from commit 7093aa60d142ac8d88faf525c7b2d616e89c3310) --- .../main/resources/dev-static/js/config.js | 5 +++- .../main/resources/dev-static/js/web/web.js | 2 +- integration-tests/devmode/pom.xml | 5 ++++ .../devconsole/DevConsoleKafkaSmokeTest.java | 29 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js index 130b130828fbe..913a6d4f8fc73 100644 --- a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js +++ b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js @@ -1,2 +1,5 @@ -export const api = '/q/dev/io.quarkus.quarkus-kafka-client/kafka-admin'; +export const api = () => { + let path = window.location.pathname + return path.replace('/kafka-dev-ui', '/kafka-admin') +} export const ui = 'kafka-ui'; \ No newline at end of file diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js index 6ba79b5c19720..c981bf9e24a86 100644 --- a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js +++ b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js @@ -2,7 +2,7 @@ import {api} from "../config.js" export function doPost(data, successCallback, errorCallback) { $.ajax({ - url: api, + url: api(), type: 'POST', data: JSON.stringify(data), contentType: "application/json; charset=utf-8", diff --git a/integration-tests/devmode/pom.xml b/integration-tests/devmode/pom.xml index b6ec10f4092e8..6a6c2b5ce8521 100644 --- a/integration-tests/devmode/pom.xml +++ b/integration-tests/devmode/pom.xml @@ -34,6 +34,11 @@ quarkus-grpc-deployment test + + io.quarkus + quarkus-kafka-client-deployment + test + io.quarkus quarkus-resteasy-reactive-qute-deployment diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java new file mode 100644 index 0000000000000..bbf82bf28ab1b --- /dev/null +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java @@ -0,0 +1,29 @@ +package io.quarkus.test.devconsole; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +/** + * Note that this test cannot be placed under the relevant {@code -deployment} module because then the DEV UI processor would + * not be able to locate the template resources correctly. + */ +public class DevConsoleKafkaSmokeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot( + (jar) -> jar.addAsResource(new StringAsset("quarkus.http.root-path=testing"), "application.properties")); + + @Test + public void testServices() { + RestAssured.get("testing/q/dev/io.quarkus.quarkus-kafka-client/kafka-dev-ui") + .then() + .statusCode(200).body(Matchers.containsString("Kafka Dev UI")); + } + +} From 1da3c2274c6437447561cf1dbfd3c184627bd11d Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 3 Jan 2023 12:59:51 +0200 Subject: [PATCH 16/17] Properly read the value of testcontainers.reuse.enable As mentioned by @LaunchPairs in the analysis of #30059, the use of TestcontainersConfiguration#getUserProperty results in using only environment variables as the source for reading the testcontainers.reuse.enable. We should instead also read the value of ~/.testcontainers.properties in order to ensure we write back the proper value when restoring Fixes: #30059 (cherry picked from commit 0ff1d5d4e833abbfa3055a4dc39a1e09e77ce8ba) --- .../src/main/java/io/quarkus/deployment/IsDockerWorking.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index c504de2b24410..2b26c34844365 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -88,7 +88,7 @@ public Result get() { .loadClass("org.testcontainers.utility.TestcontainersConfiguration"); Object configurationInstance = configurationClass.getMethod("getInstance").invoke(null); String oldReusePropertyValue = (String) configurationClass - .getMethod("getUserProperty", String.class, String.class) + .getMethod("getEnvVarOrUserProperty", String.class, String.class) .invoke(configurationInstance, "testcontainers.reuse.enable", "false"); // use the default provided in TestcontainersConfiguration#environmentSupportsReuse Method updateUserConfigMethod = configurationClass.getMethod("updateUserConfig", String.class, String.class); // this will ensure that testcontainers does not start ryuk - see https://github.com/quarkusio/quarkus/issues/25852 for why this is important From 02774a47ed36d7824c56dee79b6eb417c376b5fe Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 3 Jan 2023 18:48:08 +0200 Subject: [PATCH 17/17] Ensure that k8s client's ExecConfig works properly in native mode Fixes: #30151 (cherry picked from commit 0491d6f70d006fad2eda57a85cb0123f17f94e4c) --- .../client/deployment/KubernetesClientProcessor.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java index d171af78abf74..3228c1b60afef 100644 --- a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java +++ b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java @@ -207,6 +207,13 @@ public void process(ApplicationIndexBuildItem applicationIndex, CombinedIndexBui reflectiveClasses .produce(new ReflectiveClassBuildItem(true, true, VersionInfo.class.getName())); + // exec credentials support - we need to use Strings as the classes are private + reflectiveClasses + .produce(new ReflectiveClassBuildItem(true, true, + "io.fabric8.kubernetes.client.Config$ExecCredential", + "io.fabric8.kubernetes.client.Config$ExecCredentialSpec", + "io.fabric8.kubernetes.client.Config$ExecCredentialStatus")); + if (log.isDebugEnabled()) { final String watchedClassNames = watchedClasses .stream().map(Object::toString)