From 217a09ce46bac48b7492a881e2ca5fbeef9dfd6d Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Mon, 14 Aug 2023 10:11:43 -0300 Subject: [PATCH] Switch to Resteasy Reactive Closes #10713 --- .../org/keycloak/common/util/UriUtils.java | 12 +- .../src/main/resources/META-INF/beans.xml | 0 .../src/main/resources/META-INF/beans.xml | 0 quarkus/deployment/pom.xml | 4 +- .../quarkus/deployment/KeycloakProcessor.java | 56 ++---- quarkus/runtime/pom.xml | 4 +- .../quarkus/runtime/KeycloakRecorder.java | 24 --- .../runtime/configuration/Configuration.java | 10 + .../hostname/DefaultHostnameProvider.java | 8 +- .../integration/QuarkusHttpRequest.java | 58 ------ .../integration/QuarkusKeycloakSession.java | 3 +- .../integration/QuarkusLifecycleObserver.java | 49 ----- .../runtime/integration/QuarkusPlatform.java | 15 +- .../KeycloakBeanProducer.java} | 22 ++- .../ClientConnectionContextInjector.java | 46 ----- ...seFilter.java => CloseSessionHandler.java} | 33 +++- .../jaxrs/EmptyMultivaluedMap.java | 124 +++++++++++++ ...va => KeycloakSessionContextInjector.java} | 5 +- .../jaxrs/QuarkusKeycloakApplication.java | 43 +++-- .../jaxrs/QuarkusObjectMapperResolver.java | 34 ++++ .../TransactionalResponseInterceptor.java | 49 ----- .../resteasy/CreateSessionHandler.java | 51 +++++ .../KeycloakHandlerChainCustomizer.java | 65 +++++++ .../resteasy/QuarkusClientConnection.java | 56 ++++++ .../resteasy/QuarkusHttpRequest.java | 152 +++++++++++++++ .../resteasy/QuarkusHttpResponse.java | 146 +++++++++++++++ .../resteasy/QuarkusKeycloakContext.java | 71 +++++++ .../ResteasyVertxProvider.java | 20 +- .../SetResponseContentTypeHandler.java | 45 +++++ .../integration/web/QuarkusRequestFilter.java | 175 ------------------ .../DebugHostnameSettingsResource.java | 10 +- .../TransactionalSessionHandler.java | 5 +- .../org.keycloak.common.util.ResteasyProvider | 2 +- .../src/main/resources/application.properties | 9 +- quarkus/server/pom.xml | 9 +- quarkus/tests/integration/pom.xml | 4 + .../it/cli/dist/HostnameDistTest.java | 52 +++--- .../keycloak/it/cli/dist/LoggingDistTest.java | 6 +- .../org/keycloak/models/KeycloakUriInfo.java | 21 ++- .../keycloak/platform/PlatformProvider.java | 8 +- .../services/DefaultKeycloakContext.java | 6 +- .../services/error/KeycloakErrorHandler.java | 30 ++- ...ycloakMismatchedInputExceptionHandler.java | 45 +++++ .../resources/KeycloakApplication.java | 12 +- .../java/org/keycloak/utils/OAuth2Error.java | 2 +- .../error/UncaughtErrorPageTest.java | 5 +- .../federation/ldap/LDAPAdminRestApiTest.java | 40 ++++ 47 files changed, 1083 insertions(+), 563 deletions(-) create mode 100644 federation/ldap/src/main/resources/META-INF/beans.xml create mode 100644 model/legacy-services/src/main/resources/META-INF/beans.xml delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusHttpRequest.java delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusLifecycleObserver.java rename quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/{QuarkusKeycloakContext.java => cdi/KeycloakBeanProducer.java} (62%) delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ClientConnectionContextInjector.java rename quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/{TransactionalResponseFilter.java => CloseSessionHandler.java} (65%) create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/EmptyMultivaluedMap.java rename quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/{KeycloakContextInjector.java => KeycloakSessionContextInjector.java} (88%) create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusObjectMapperResolver.java delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseInterceptor.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusClientConnection.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpRequest.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpResponse.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusKeycloakContext.java rename quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/{jaxrs => resteasy}/ResteasyVertxProvider.java (76%) create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/SetResponseContentTypeHandler.java delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java create mode 100644 services/src/main/java/org/keycloak/services/error/KeycloakMismatchedInputExceptionHandler.java diff --git a/common/src/main/java/org/keycloak/common/util/UriUtils.java b/common/src/main/java/org/keycloak/common/util/UriUtils.java index d8cc910d60..df386c3139 100755 --- a/common/src/main/java/org/keycloak/common/util/UriUtils.java +++ b/common/src/main/java/org/keycloak/common/util/UriUtils.java @@ -58,7 +58,7 @@ public class UriUtils { } } - public static MultivaluedHashMap decodeQueryString(String queryString) { + public static MultivaluedHashMap parseQueryParameters(String queryString, boolean decode) { MultivaluedHashMap map = new MultivaluedHashMap(); if (queryString == null || queryString.equals("")) return map; @@ -71,9 +71,9 @@ public class UriUtils { String[] nv = param.split("=", 2); try { - String name = URLDecoder.decode(nv[0], "UTF-8"); + String name = decode ? URLDecoder.decode(nv[0], "UTF-8") : nv[0]; String val = nv.length > 1 ? nv[1] : ""; - map.add(name, URLDecoder.decode(val, "UTF-8")); + map.add(name, decode ? URLDecoder.decode(val, "UTF-8") : val); } catch (UnsupportedEncodingException e) { @@ -84,7 +84,7 @@ public class UriUtils { { try { - String name = URLDecoder.decode(param, "UTF-8"); + String name = decode ? URLDecoder.decode(param, "UTF-8") : param; map.add(name, ""); } catch (UnsupportedEncodingException e) @@ -96,6 +96,10 @@ public class UriUtils { return map; } + public static MultivaluedHashMap decodeQueryString(String queryString) { + return parseQueryParameters(queryString, true); + } + public static String stripQueryParam(String url, String name){ return url.replaceFirst("[\\?&]"+name+"=[^&]*$|"+name+"=[^&]*&", ""); } diff --git a/federation/ldap/src/main/resources/META-INF/beans.xml b/federation/ldap/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/model/legacy-services/src/main/resources/META-INF/beans.xml b/model/legacy-services/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml index 4ba1b0a01c..326e1d9165 100644 --- a/quarkus/deployment/pom.xml +++ b/quarkus/deployment/pom.xml @@ -49,11 +49,11 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment io.quarkus - quarkus-resteasy-jackson-deployment + quarkus-resteasy-reactive-jackson-deployment io.quarkus diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 7c3184d84b..4006358135 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -40,11 +40,9 @@ import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig; import io.quarkus.hibernate.orm.deployment.PersistenceXmlDescriptorBuildItem; import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem; import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem; -import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentCustomizerBuildItem; +import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.configuration.ProfileManager; -import io.quarkus.vertx.http.deployment.FilterBuildItem; -import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.smallrye.config.ConfigValue; import org.hibernate.cfg.AvailableSettings; @@ -55,8 +53,10 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; -import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; import org.jboss.resteasy.spi.ResteasyDeployment; import org.keycloak.Config; import org.keycloak.authentication.AuthenticatorSpi; @@ -93,7 +93,7 @@ import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; -import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; +import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer; import org.keycloak.quarkus.runtime.integration.web.NotFoundHandler; import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck; import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory; @@ -101,6 +101,7 @@ import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFac import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.representations.provider.ScriptProviderMetadata; import org.keycloak.services.ServicesLogger; +import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.theme.ClasspathThemeProviderFactory; import org.keycloak.theme.ClasspathThemeResourceProviderFactory; import org.keycloak.theme.FolderThemeProviderFactory; @@ -181,10 +182,6 @@ class KeycloakProcessor { ClasspathThemeResourceProviderFactory.class, JarThemeProviderFactory.class, JpaMapStorageProviderFactory.class); - public static final String QUARKUS_HEALTH_ROOT_PROPERTY = "quarkus.smallrye-health.root-path"; - public static final String QUARKUS_METRICS_PATH_PROPERTY = "quarkus.micrometer.export.prometheus.path"; - public static final String QUARKUS_DEFAULT_HEALTH_PATH = "health"; - public static final String QUARKUS_DEFAULT_METRICS_PATH = "metrics"; static { DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator); @@ -593,27 +590,6 @@ class KeycloakProcessor { indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-jpa")); } - @Record(ExecutionTime.RUNTIME_INIT) - @BuildStep - void initializeFilter(BuildProducer filters, KeycloakRecorder recorder, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { - - List ignoredPaths = new ArrayList<>(); - - if (isHealthEnabled()) { - ignoredPaths.add(nonApplicationRootPathBuildItem. - resolvePath(getOptionalValue(QUARKUS_HEALTH_ROOT_PROPERTY) - .orElse(QUARKUS_DEFAULT_HEALTH_PATH))); - } - - if (isMetricsEnabled()) { - ignoredPaths.add(nonApplicationRootPathBuildItem. - resolvePath(getOptionalValue(QUARKUS_METRICS_PATH_PROPERTY) - .orElse(QUARKUS_DEFAULT_METRICS_PATH))); - } - - filters.produce(new FilterBuildItem(recorder.createRequestFilter(ignoredPaths),FilterBuildItem.AUTHORIZATION - 10)); - } - @BuildStep void disableMetricsEndpoint(BuildProducer routes) { if (!isMetricsEnabled()) { @@ -641,15 +617,19 @@ class KeycloakProcessor { } @BuildStep - void configureResteasy(BuildProducer deploymentCustomizerProducer) { - deploymentCustomizerProducer.produce(new ResteasyDeploymentCustomizerBuildItem(new Consumer() { + void configureResteasy(CombinedIndexBuildItem index, + BuildProducer buildTimeConditionBuildItemBuildProducer, + BuildProducer scanner) { + buildTimeConditionBuildItemBuildProducer.produce(new BuildTimeConditionBuildItem(index.getIndex().getClassByName(DotName.createSimple( + KeycloakApplication.class.getName())), false)); + + KeycloakHandlerChainCustomizer chainCustomizer = new KeycloakHandlerChainCustomizer(); + + scanner.produce(new MethodScannerBuildItem(new MethodScanner() { @Override - public void accept(ResteasyDeployment resteasyDeployment) { - // we need to explicitly set the application to avoid errors at build time due to the application - // from keycloak-services also being added to the index - resteasyDeployment.setApplicationClass(QuarkusKeycloakApplication.class.getName()); - // we need to disable the sanitizer to avoid escaping text/html responses from the server - resteasyDeployment.setProperty(ResteasyContextParameters.RESTEASY_DISABLE_HTML_SANITIZER, Boolean.TRUE); + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + return List.of(chainCustomizer); } })); } diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index ad1a9e9e85..6c801e9446 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -41,11 +41,11 @@ io.quarkus - quarkus-resteasy + quarkus-resteasy-reactive io.quarkus - quarkus-resteasy-jackson + quarkus-resteasy-reactive-jackson io.quarkus diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index c333f8ae54..269b9f677e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -42,7 +42,6 @@ import org.keycloak.common.crypto.FipsMode; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; -import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter; import org.keycloak.quarkus.runtime.storage.database.liquibase.FastServiceLocator; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; @@ -141,29 +140,6 @@ public class KeycloakRecorder { }; } - public QuarkusRequestFilter createRequestFilter(List ignoredPaths) { - return new QuarkusRequestFilter(createIgnoredHttpPathsPredicate(ignoredPaths)); - } - - private Predicate createIgnoredHttpPathsPredicate(List ignoredPaths) { - if (ignoredPaths == null || ignoredPaths.isEmpty()) { - return null; - } - - return new Predicate<>() { - @Override - public boolean test(RoutingContext context) { - for (String ignoredPath : ignoredPaths) { - if (context.request().uri().startsWith(ignoredPath)) { - return true; - } - } - - return false; - } - }; - } - public void setCryptoProvider(FipsMode fipsMode) { String cryptoProvider = fipsMode.getProviderClassName(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java index 87adc0595a..2e276174f8 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java @@ -107,6 +107,16 @@ public final class Configuration { return getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName)); } + public static Optional getOptionalBooleanKcValue(String propertyName) { + Optional value = getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName)); + + if (value.isPresent()) { + return value.map(Boolean::parseBoolean); + } + + return Optional.empty(); + } + public static Optional getOptionalBooleanValue(String name) { return getOptionalValue(name).map(Boolean::parseBoolean); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/hostname/DefaultHostnameProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/hostname/DefaultHostnameProvider.java index 430f83df6b..c4bb9085d7 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/hostname/DefaultHostnameProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/hostname/DefaultHostnameProvider.java @@ -174,6 +174,11 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname protected URI getRealmFrontEndUrl() { KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); + + if (session == null) { + return null; + } + RealmModel realm = session.getContext().getRealm(); if (realm == null) { @@ -300,8 +305,7 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname } private int getRequestPort(UriInfo uriInfo) { - KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); - return session.getContext().getHttpRequest().getUri().getBaseUri().getPort(); + return uriInfo.getBaseUri().getPort(); } private T fromBaseUriOrDefault(Function resolver, URI baseUri, T defaultValue) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusHttpRequest.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusHttpRequest.java deleted file mode 100644 index 8adebf7c21..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusHttpRequest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.integration; - -import java.security.cert.X509Certificate; -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.spi.CDI; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.services.HttpRequestImpl; - -import io.vertx.ext.web.RoutingContext; - -public class QuarkusHttpRequest extends HttpRequestImpl { - - public QuarkusHttpRequest(HttpRequest delegate) { - super(delegate); - } - - @Override - public X509Certificate[] getClientCertificateChain() { - Instance instances = CDI.current().select(RoutingContext.class); - - if (instances.isResolvable()) { - RoutingContext context = instances.get(); - - try { - SSLSession sslSession = context.request().sslSession(); - - if (sslSession == null) { - return null; - } - - return (X509Certificate[]) sslSession.getPeerCertificates(); - } catch (SSLPeerUnverifiedException ignore) { - // client not authenticated - } - } - - return null; - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSession.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSession.java index 4c42d12ee5..5262dbd970 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSession.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSession.java @@ -18,11 +18,12 @@ package org.keycloak.quarkus.runtime.integration; import org.keycloak.models.KeycloakSession; +import org.keycloak.quarkus.runtime.integration.resteasy.QuarkusKeycloakContext; import org.keycloak.services.DefaultKeycloakContext; import org.keycloak.services.DefaultKeycloakSession; import org.keycloak.services.DefaultKeycloakSessionFactory; -public class QuarkusKeycloakSession extends DefaultKeycloakSession { +public final class QuarkusKeycloakSession extends DefaultKeycloakSession { public QuarkusKeycloakSession(DefaultKeycloakSessionFactory factory) { super(factory); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusLifecycleObserver.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusLifecycleObserver.java deleted file mode 100644 index a7580c4e9c..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusLifecycleObserver.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.integration; - -import io.quarkus.runtime.ShutdownEvent; -import io.quarkus.runtime.StartupEvent; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; - -import org.keycloak.platform.Platform; - -@ApplicationScoped -public class QuarkusLifecycleObserver { - - void onStartupEvent(@Observes StartupEvent event) { - QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform(); - platform.started(); - QuarkusPlatform.exitOnError(); - Runnable startupHook = platform.startupHook; - - if (startupHook != null) { - startupHook.run(); - } - } - - void onShutdownEvent(@Observes ShutdownEvent event) { - - Runnable shutdownHook = ((QuarkusPlatform) Platform.getPlatform()).shutdownHook; - - if (shutdownHook != null) - shutdownHook.run(); - - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java index cad2c8dd20..6f0d25027c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java @@ -77,23 +77,10 @@ public class QuarkusPlatform implements PlatformProvider { } } - Runnable startupHook; - Runnable shutdownHook; - private AtomicBoolean started = new AtomicBoolean(false); private List deferredExceptions = new CopyOnWriteArrayList<>(); private File tmpDir; - @Override - public void onStartup(Runnable startupHook) { - this.startupHook = startupHook; - } - - @Override - public void onShutdown(Runnable shutdownHook) { - this.shutdownHook = shutdownHook; - } - @Override public void exit(Throwable cause) { Quarkus.asyncExit(1); @@ -102,7 +89,7 @@ public class QuarkusPlatform implements PlatformProvider { /** * Called when Quarkus platform is started */ - void started() { + public void started() { this.started.set(true); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakContext.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/cdi/KeycloakBeanProducer.java similarity index 62% rename from quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakContext.java rename to quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/cdi/KeycloakBeanProducer.java index ebd5e0bc0a..f5f37ba792 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakContext.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/cdi/KeycloakBeanProducer.java @@ -15,21 +15,23 @@ * limitations under the License. */ -package org.keycloak.quarkus.runtime.integration; +package org.keycloak.quarkus.runtime.integration.cdi; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Produces; import org.keycloak.common.util.Resteasy; -import org.keycloak.http.HttpRequest; import org.keycloak.models.KeycloakSession; -import org.keycloak.services.DefaultKeycloakContext; -public class QuarkusKeycloakContext extends DefaultKeycloakContext { +import io.quarkus.arc.Unremovable; - public QuarkusKeycloakContext(KeycloakSession session) { - super(session); - } +@ApplicationScoped +@Unremovable +public class KeycloakBeanProducer { - @Override - protected HttpRequest createHttpRequest() { - return new QuarkusHttpRequest(Resteasy.getContextData(org.jboss.resteasy.spi.HttpRequest.class)); + @Produces + @RequestScoped + public KeycloakSession getKeycloakSession() { + return Resteasy.getContextData(KeycloakSession.class); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ClientConnectionContextInjector.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ClientConnectionContextInjector.java deleted file mode 100644 index f9cac319f5..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ClientConnectionContextInjector.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.integration.jaxrs; - -import jakarta.ws.rs.ext.Provider; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -import org.jboss.resteasy.spi.ContextInjector; -import org.keycloak.common.ClientConnection; -import org.keycloak.common.util.Resteasy; -import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter; - -/** - *

This {@link ContextInjector} allows injecting {@link ClientConnection} to JAX-RS resources. - * - *

Due to the latest changes in Quarkus, the context map is cleared prior to dispatching to JAX-RS resources, so we need - * to delegate to the {@link ResteasyVertxProvider} provider the lookup of Keycloak contextual objects. - * - * @see QuarkusRequestFilter - * @see ResteasyVertxProvider - * - * @author Pedro Igor - */ -@Provider -public class ClientConnectionContextInjector implements ContextInjector { - @Override - public ClientConnection resolve(Class rawType, Type genericType, Annotation[] annotations) { - return Resteasy.getContextData(ClientConnection.class); - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseFilter.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/CloseSessionHandler.java similarity index 65% rename from quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseFilter.java rename to quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/CloseSessionHandler.java index 7650bcb7e1..efac9e23fc 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseFilter.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/CloseSessionHandler.java @@ -18,7 +18,10 @@ package org.keycloak.quarkus.runtime.integration.jaxrs; import java.io.IOException; +import java.io.OutputStream; import java.util.stream.Stream; +import jakarta.annotation.Priority; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; @@ -29,28 +32,40 @@ import org.keycloak.common.util.Resteasy; import org.keycloak.models.KeycloakSession; import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler; -import jakarta.annotation.Priority; - @Provider @PreMatching @Priority(1) -public class TransactionalResponseFilter implements ContainerResponseFilter, TransactionalSessionHandler { +public class CloseSessionHandler implements ContainerResponseFilter, TransactionalSessionHandler { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { Object entity = responseContext.getEntity(); - if (shouldDelaySessionClose(entity)) { + if (entity instanceof Stream) { + Stream entityStream = (Stream) entity; + entityStream.onClose(this::closeSession); return; } - close(Resteasy.getContextData(KeycloakSession.class)); + if (entity instanceof StreamingOutput) { + responseContext.setEntity(new StreamingOutput() { + @Override + public void write(OutputStream output) throws IOException, WebApplicationException { + try { + ((StreamingOutput) entity).write(output); + } finally { + closeSession(); + } + } + }); + return; + } + + closeSession(); } - private static boolean shouldDelaySessionClose(Object entity) { - // do not close the session if the response entity is a stream - // that is because we need the session open until the stream is transformed as it might require access to the database - return entity instanceof Stream || entity instanceof StreamingOutput; + private void closeSession() { + close(Resteasy.getContextData(KeycloakSession.class)); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/EmptyMultivaluedMap.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/EmptyMultivaluedMap.java new file mode 100644 index 0000000000..9ebbc3f5b5 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/EmptyMultivaluedMap.java @@ -0,0 +1,124 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.jaxrs; + +import static java.util.Collections.emptySet; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import jakarta.ws.rs.core.MultivaluedMap; + +public final class EmptyMultivaluedMap implements MultivaluedMap { + + @Override + public void putSingle(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V getFirst(K key) { + return null; + } + + @Override + public void addAll(K key, V... newValues) { + throw new UnsupportedOperationException(); + } + + @Override + public void addAll(K key, List valueList) { + throw new UnsupportedOperationException(); + } + + @Override + public void addFirst(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equalsIgnoreValueOrder(MultivaluedMap otherMap) { + return false; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean containsKey(Object key) { + return false; + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Override + public List get(Object key) { + return null; + } + + @Override + public List put(K key, List value) { + throw new UnsupportedOperationException(); + } + + @Override + public List remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map> m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() { + return emptySet(); + } + + @Override + public Collection> values() { + return emptySet(); + } + + @Override + public Set>> entrySet() { + return emptySet(); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/KeycloakContextInjector.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/KeycloakSessionContextInjector.java similarity index 88% rename from quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/KeycloakContextInjector.java rename to quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/KeycloakSessionContextInjector.java index da0cb0fddb..54fef01299 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/KeycloakContextInjector.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/KeycloakSessionContextInjector.java @@ -24,7 +24,7 @@ import java.lang.reflect.Type; import org.jboss.resteasy.spi.ContextInjector; import org.keycloak.common.util.Resteasy; import org.keycloak.models.KeycloakSession; -import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter; +import org.keycloak.quarkus.runtime.integration.resteasy.ResteasyVertxProvider; /** *

This {@link ContextInjector} allows injecting {@link KeycloakSession} to JAX-RS resources. @@ -32,13 +32,12 @@ import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter; *

Due to the latest changes in Quarkus, the context map is cleared prior to dispatching to JAX-RS resources, so we need * to delegate to the {@link ResteasyVertxProvider} provider the lookup of Keycloak contextual objects. * - * @see QuarkusRequestFilter * @see ResteasyVertxProvider * * @author Pedro Igor */ @Provider -public class KeycloakContextInjector implements ContextInjector { +public class KeycloakSessionContextInjector implements ContextInjector { @Override public KeycloakSession resolve(Class rawType, Type genericType, Annotation[] annotations) { return Resteasy.getContextData(KeycloakSession.class); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java index dfdc582d60..6fc50eb66b 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java @@ -17,26 +17,40 @@ package org.keycloak.quarkus.runtime.integration.jaxrs; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; +import jakarta.enterprise.event.Observes; import jakarta.ws.rs.ApplicationPath; import org.keycloak.config.HostnameOptions; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.platform.Platform; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; +import org.keycloak.quarkus.runtime.integration.QuarkusPlatform; import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource; -import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.WelcomeResource; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import io.smallrye.common.annotation.Blocking; + @ApplicationPath("/") +@Blocking public class QuarkusKeycloakApplication extends KeycloakApplication { - private static boolean filterSingletons(Object o) { - return !WelcomeResource.class.isInstance(o); + void onStartupEvent(@Observes StartupEvent event) { + QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform(); + platform.started(); + QuarkusPlatform.exitOnError(); + startup(); + } + + void onShutdownEvent(@Observes ShutdownEvent event) { + shutdown(); } @Override @@ -53,16 +67,21 @@ public class QuarkusKeycloakApplication extends KeycloakApplication { @Override public Set getSingletons() { - Set singletons = super.getSingletons().stream() - .filter(QuarkusKeycloakApplication::filterSingletons) - .collect(Collectors.toSet()); + return Set.of(); + } - singletons.add(new QuarkusWelcomeResource()); + @Override + public Set> getClasses() { + Set> classes = new HashSet<>(super.getClasses()); - if (Configuration.getOptionalBooleanValue("--" + HostnameOptions.HOSTNAME_DEBUG.getKey()).orElse(Boolean.FALSE)) { - singletons.add(new DebugHostnameSettingsResource()); - } + classes.remove(WelcomeResource.class); + classes.add(QuarkusWelcomeResource.class); - return singletons; + classes.add(QuarkusObjectMapperResolver.class); + classes.add(CloseSessionHandler.class); + + classes.add(DebugHostnameSettingsResource.class); + + return classes; } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusObjectMapperResolver.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusObjectMapperResolver.java new file mode 100644 index 0000000000..00e23bd734 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusObjectMapperResolver.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.jaxrs; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.ws.rs.ext.Provider; +import org.keycloak.services.util.ObjectMapperResolver; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Provider +@ApplicationScoped +public class QuarkusObjectMapperResolver extends ObjectMapperResolver { + + @Produces + public ObjectMapper getObjectMapper() { + return mapper; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseInterceptor.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseInterceptor.java deleted file mode 100644 index bd18159f98..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/TransactionalResponseInterceptor.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.integration.jaxrs; - -import java.io.IOException; -import jakarta.ws.rs.ConstrainedTo; -import jakarta.ws.rs.RuntimeType; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.ext.Provider; -import jakarta.ws.rs.ext.WriterInterceptor; -import jakarta.ws.rs.ext.WriterInterceptorContext; -import org.keycloak.common.util.Resteasy; -import org.keycloak.models.KeycloakSession; -import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler; - -import jakarta.annotation.Priority; - -@Provider -@ConstrainedTo(RuntimeType.SERVER) -@Priority(10000) -public class TransactionalResponseInterceptor implements WriterInterceptor, TransactionalSessionHandler { - @Override - public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { - KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); - - try { - context.proceed(); - } finally { - // make sure response is closed after writing to the response output stream - // this is needed in order to support streams from endpoints as they need access to underlying resources like database - close(session); - } - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java new file mode 100644 index 0000000000..c3bbea9651 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import static org.keycloak.common.util.Resteasy.clearContextData; + +import jakarta.ws.rs.container.CompletionCallback; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; +import org.keycloak.models.KeycloakSession; +import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler; + +import io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext; +import io.vertx.ext.web.RoutingContext; + +public final class CreateSessionHandler implements ServerRestHandler, TransactionalSessionHandler, CompletionCallback { + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) { + QuarkusResteasyReactiveRequestContext context = (QuarkusResteasyReactiveRequestContext) requestContext; + RoutingContext routingContext = context.getContext(); + KeycloakSession currentSession = routingContext.get(KeycloakSession.class.getName()); + + if (currentSession == null) { + // this handler might be invoked multiple times when resolving sub-resources + // make sure the session is created once + routingContext.put(KeycloakSession.class.getName(), create()); + context.registerCompletionCallback(this); + } + } + + @Override + public void onComplete(Throwable throwable) { + clearContextData(); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java new file mode 100644 index 0000000000..a615cc2af8 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import static jakarta.ws.rs.HttpMethod.POST; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import org.jboss.resteasy.reactive.common.model.ResourceClass; +import org.jboss.resteasy.reactive.server.handlers.FormBodyHandler; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.ServerResourceMethod; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +public final class KeycloakHandlerChainCustomizer implements HandlerChainCustomizer { + + private final CreateSessionHandler TRANSACTIONAL_SESSION_HANDLER = new CreateSessionHandler(); + + private final FormBodyHandler formBodyHandler = new FormBodyHandler(true, new Supplier() { + @Override + public Executor get() { + // we always run in blocking mode and never run in an event loop thread + // we don't need to provide an executor to dispatch to a worker thread to parse the body + return null; + } + }, Set.of()); + + @Override + public List handlers(Phase phase, ResourceClass resourceClass, + ServerResourceMethod resourceMethod) { + List handlers = new ArrayList<>(); + + switch (phase) { + case BEFORE_METHOD_INVOKE: + if (POST.equalsIgnoreCase(resourceMethod.getHttpMethod())) { + handlers.add(formBodyHandler); + } + handlers.add(TRANSACTIONAL_SESSION_HANDLER); + break; + case AFTER_METHOD_INVOKE: + handlers.add(new SetResponseContentTypeHandler(resourceMethod.getProduces())); + } + + return handlers; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusClientConnection.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusClientConnection.java new file mode 100644 index 0000000000..ddbea53514 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusClientConnection.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import org.keycloak.common.ClientConnection; + +import io.vertx.core.http.HttpServerRequest; + +public final class QuarkusClientConnection implements ClientConnection { + + private final HttpServerRequest request; + + public QuarkusClientConnection(HttpServerRequest request) { + this.request = request; + } + + @Override + public String getRemoteAddr() { + return request.remoteAddress().host(); + } + + @Override + public String getRemoteHost() { + return request.remoteAddress().host(); + } + + @Override + public int getRemotePort() { + return request.remoteAddress().port(); + } + + @Override + public String getLocalAddr() { + return request.localAddress().host(); + } + + @Override + public int getLocalPort() { + return request.localAddress().port(); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpRequest.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpRequest.java new file mode 100644 index 0000000000..c70ecfbb1d --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpRequest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Deque; +import java.util.Iterator; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.FormData; +import org.jboss.resteasy.reactive.server.multipart.FormValue; +import org.keycloak.http.FormPartValue; +import org.keycloak.http.HttpRequest; +import org.keycloak.quarkus.runtime.integration.jaxrs.EmptyMultivaluedMap; +import org.keycloak.services.FormPartValueImpl; + +import io.vertx.ext.web.RoutingContext; + +public final class QuarkusHttpRequest implements HttpRequest { + + private static final MultivaluedMap EMPTY_FORM_PARAM = new EmptyMultivaluedMap<>(); + private static final MultivaluedMap EMPTY_MULTI_MAP_MULTI_PART = new EmptyMultivaluedMap<>(); + + private final ResteasyReactiveRequestContext context; + + public QuarkusHttpRequest(ResteasyReactiveRequestContext context) { + this.context = context; + } + + @Override + public String getHttpMethod() { + return context.getMethod(); + } + + @Override + public MultivaluedMap getDecodedFormParameters() { + FormData parameters = context.getFormData(); + + if (parameters == null || !parameters.iterator().hasNext()) { + return EMPTY_FORM_PARAM; + } + + MultivaluedMap params = new QuarkusMultivaluedHashMap<>(); + + for (String name : parameters) { + Deque values = parameters.get(name); + + if (values == null || values.isEmpty()) { + continue; + } + + for (FormValue value : values) { + params.add(name, value.getValue()); + } + } + + return params; + } + + @Override + public MultivaluedMap getMultiPartFormParameters() { + FormData formData = context.getFormData(); + + if (formData == null) { + return EMPTY_MULTI_MAP_MULTI_PART; + } + + MultivaluedMap params = new QuarkusMultivaluedHashMap<>(); + + for (String name : formData) { + Deque formValues = formData.get(name); + + if (formValues != null) { + Iterator iterator = formValues.iterator(); + + while (iterator.hasNext()) { + FormValue formValue = iterator.next(); + + if (formValue.isFileItem()) { + try { + params.add(name, new FormPartValueImpl(formValue.getFileItem().getInputStream())); + } catch (IOException cause) { + throw new RuntimeException("Failed to parse multipart file parameter", cause); + } + } else { + params.add(name, new FormPartValueImpl(formValue.getValue())); + } + } + } + } + + return params; + } + + @Override + public HttpHeaders getHttpHeaders() { + return context.getHttpHeaders(); + } + + @Override + public X509Certificate[] getClientCertificateChain() { + Instance instances = CDI.current().select(RoutingContext.class); + + if (instances.isResolvable()) { + RoutingContext context = instances.get(); + + try { + SSLSession sslSession = context.request().sslSession(); + + if (sslSession == null) { + return null; + } + + return (X509Certificate[]) sslSession.getPeerCertificates(); + } catch (SSLPeerUnverifiedException ignore) { + // client not authenticated + } + } + + return null; + } + + @Override + public UriInfo getUri() { + return context.getUriInfo(); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpResponse.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpResponse.java new file mode 100644 index 0000000000..5b48164a8c --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusHttpResponse.java @@ -0,0 +1,146 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext; +import org.keycloak.http.HttpCookie; +import org.keycloak.http.HttpResponse; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakTransaction; + +public final class QuarkusHttpResponse implements HttpResponse, KeycloakTransaction { + + private final ResteasyReactiveRequestContext requestContext; + + private Set cookies; + private boolean transactionActive; + private boolean writeCookiesOnTransactionComplete; + + public QuarkusHttpResponse(KeycloakSession session, ResteasyReactiveRequestContext requestContext) { + this.requestContext = requestContext; + session.getTransactionManager().enlistAfterCompletion(this); + } + + @Override + public int getStatus() { + VertxResteasyReactiveRequestContext serverHttpResponse = (VertxResteasyReactiveRequestContext) requestContext.serverResponse(); + return serverHttpResponse.vertxServerResponse().getStatusCode(); + } + + @Override + public void setStatus(int statusCode) { + requestContext.serverResponse().setStatusCode(statusCode); + } + + @Override + public void addHeader(String name, String value) { + requestContext.serverResponse().addResponseHeader(name, value); + } + + @Override + public void setHeader(String name, String value) { + requestContext.serverResponse().setResponseHeader(name, value); + } + + @Override + public void setCookieIfAbsent(HttpCookie cookie) { + if (cookie == null) { + throw new IllegalArgumentException("Cookie is null"); + } + + if (cookies == null) { + cookies = new HashSet<>(); + } + + if (cookies.add(cookie)) { + if (writeCookiesOnTransactionComplete) { + // cookies are written after transaction completes + return; + } + + addHeader(HttpHeaders.SET_COOKIE, cookie.toHeaderValue()); + } + } + + @Override + public void setWriteCookiesOnTransactionComplete() { + this.writeCookiesOnTransactionComplete = true; + } + + @Override + public void begin() { + transactionActive = true; + } + + @Override + public void commit() { + if (!transactionActive) { + throw new IllegalStateException("Transaction not active. Response already committed or rolled back"); + } + + try { + addCookiesAfterTransaction(); + } finally { + close(); + } + } + + @Override + public void rollback() { + close(); + } + + @Override + public void setRollbackOnly() { + + } + + @Override + public boolean getRollbackOnly() { + return false; + } + + @Override + public boolean isActive() { + return transactionActive; + } + + private void close() { + transactionActive = false; + cookies = null; + } + + private void addCookiesAfterTransaction() { + if (cookies == null || !writeCookiesOnTransactionComplete) { + return; + } + + // Ensure that cookies are only added when the transaction is complete, as otherwise cookies will be set for + // error pages, or will be added twice when running retries. + for (HttpCookie cookie : cookies) { + addHeader(HttpHeaders.SET_COOKIE, cookie.toHeaderValue()); + } + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusKeycloakContext.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusKeycloakContext.java new file mode 100644 index 0000000000..348e0c0345 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/QuarkusKeycloakContext.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.keycloak.common.ClientConnection; +import org.keycloak.common.util.Resteasy; +import org.keycloak.http.HttpRequest; +import org.keycloak.http.HttpResponse; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.DefaultKeycloakContext; + +import io.vertx.core.http.HttpServerRequest; + +public final class QuarkusKeycloakContext extends DefaultKeycloakContext { + + private ClientConnection clientConnection; + + public QuarkusKeycloakContext(KeycloakSession session) { + super(session); + } + + @Override + protected HttpRequest createHttpRequest() { + return new QuarkusHttpRequest(getResteasyReactiveRequestContext()); + } + + @Override + protected HttpResponse createHttpResponse() { + return new QuarkusHttpResponse(getSession(), getResteasyReactiveRequestContext()); + } + + @Override + public ClientConnection getConnection() { + if (clientConnection == null) { + ClientConnection contextualObject = Resteasy.getContextData(ClientConnection.class); + + if (contextualObject == null) { + ResteasyReactiveRequestContext requestContext = getResteasyReactiveRequestContext(); + HttpServerRequest serverRequest = requestContext.unwrap(HttpServerRequest.class); + clientConnection = new QuarkusClientConnection(serverRequest); + } else { + // in case the request is dispatched to a different thread like when using JAX-RS async responses + // in this case, we expect the client connection available as a contextual data + clientConnection = contextualObject; + } + } + + return clientConnection; + } + + private ResteasyReactiveRequestContext getResteasyReactiveRequestContext() { + return CurrentRequestManager.get(); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ResteasyVertxProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/ResteasyVertxProvider.java similarity index 76% rename from quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ResteasyVertxProvider.java rename to quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/ResteasyVertxProvider.java index 0382c3b20e..a811898d2e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/ResteasyVertxProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/ResteasyVertxProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Red Hat, Inc. and/or its affiliates + * Copyright 2022 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,15 +15,14 @@ * limitations under the License. */ -package org.keycloak.quarkus.runtime.integration.jaxrs; +package org.keycloak.quarkus.runtime.integration.resteasy; +import io.quarkus.arc.Arc; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.vertx.ext.web.RoutingContext; import org.jboss.resteasy.core.ResteasyContext; import org.keycloak.common.util.ResteasyProvider; -/** - * TODO: we should probably rely on the vert.x routing context instead of resteasy context data - */ public class ResteasyVertxProvider implements ResteasyProvider { @Override @@ -31,7 +30,7 @@ public class ResteasyVertxProvider implements ResteasyProvider { R data = ResteasyContext.getContextData(type); if (data == null) { - RoutingContext contextData = ResteasyContext.getContextData(RoutingContext.class); + RoutingContext contextData = Arc.container().instance(CurrentVertxRequest.class).get().getCurrent(); if (contextData == null) { return null; @@ -44,14 +43,13 @@ public class ResteasyVertxProvider implements ResteasyProvider { } @Override - public void pushDefaultContextObject(Class type, Object instance) { - ResteasyContext.getContextData(org.jboss.resteasy.spi.Dispatcher.class).getDefaultContextObjects() - .put(type, instance); + public void pushContext(Class type, Object instance) { + ResteasyContext.pushContext(type, instance); } @Override - public void pushContext(Class type, Object instance) { - ResteasyContext.pushContext(type, instance); + public void pushDefaultContextObject(Class type, Object instance) { + pushContext(type, instance); } @Override diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/SetResponseContentTypeHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/SetResponseContentTypeHandler.java new file mode 100644 index 0000000000..312db5433d --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/SetResponseContentTypeHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.integration.resteasy; + +import jakarta.ws.rs.core.MediaType; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +/** + *

A {@link ServerRestHandler} that set the media type produced by a JAX-RS resource method. + * + *

The main reason behind this handler is to make the response media type available to {@link org.keycloak.headers.DefaultSecurityHeadersProvider}. + */ +public class SetResponseContentTypeHandler implements ServerRestHandler { + + private MediaType producesMediaType; + + public SetResponseContentTypeHandler(String[] producesMediaTypes) { + if (producesMediaTypes.length == 0) { + this.producesMediaType = MediaType.APPLICATION_JSON_TYPE; + } else { + this.producesMediaType = MediaType.valueOf(producesMediaTypes[0]); + } + } + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) { + requestContext.setResponseContentType(producesMediaType); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java deleted file mode 100644 index d9e5728db1..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.integration.web; - -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Predicate; - -import org.apache.http.HttpStatus; -import org.jboss.logging.Logger; -import org.keycloak.common.ClientConnection; -import org.keycloak.common.util.Resteasy; -import org.keycloak.models.KeycloakSession; -import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler; - -import io.vertx.core.Handler; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.ext.web.RoutingContext; - -/** - *

This filter is responsible for managing the request lifecycle as well as setting up the necessary context to process incoming - * requests. We need this filter running on the top of the chain in order to push contextual objects before executing Resteasy. It is not - * possible to use a {@link jakarta.ws.rs.container.ContainerRequestFilter} for this purpose because some mechanisms like error handling - * will not be able to access these contextual objects. - * - *

The filter itself runs in an event loop and should delegate to worker threads any blocking code (for now, all requests are handled - * as blocking). - * - *

Note that this filter is only responsible to close the {@link KeycloakSession} if not already closed when running Resteasy code. The reason is that closing it should be done at the - * Resteasy level so that we don't block event loop threads even if they execute in a worker thread. Vert.x handlers and their - * callbacks are not designed to run blocking code. If the session is eventually closed here is because Resteasy was not executed. - * - * @see org.keycloak.quarkus.runtime.integration.jaxrs.TransactionalResponseInterceptor - * @see org.keycloak.quarkus.runtime.integration.jaxrs.TransactionalResponseFilter - */ -public class QuarkusRequestFilter implements Handler, TransactionalSessionHandler { - - private final Logger logger = Logger.getLogger(QuarkusRequestFilter.class); - - private final Predicate contextFilter; - - public QuarkusRequestFilter() { - this(null); - } - - public QuarkusRequestFilter(Predicate contextFilter) { - this.contextFilter = contextFilter; - } - - private final LongAdder rejectedRequests = new LongAdder(); - private volatile boolean loadSheddingActive; - - @Override - public void handle(RoutingContext context) { - if (ignoreContext(context)) { - context.next(); - return; - } - // our code should always be run as blocking until we don't provide a better support for running non-blocking code - // in the event loop - try { - // When running in Quarkus dev mode, this will run on Vert.x default worker pool. - // When running in Quarkus production mode, this will run on the Quarkus executor pool. - // It should not call the Quarkus executor directly, as this prevents metrics for `worker_pool_*` to be collected. - // See https://github.com/quarkusio/quarkus/issues/34998 for a discussion. - context.vertx().executeBlocking(ctx -> { - try { - runBlockingCode(context); - ctx.complete(); - } catch (Throwable ex) { - ctx.fail(ex); - } - }, false, null); - if (loadSheddingActive) { - synchronized (rejectedRequests) { - if (loadSheddingActive) { - loadSheddingActive = false; - // rejectedRequests.sumThenReset() is approximative when concurrent increments are active, still it should be accurate enough for this log message - logger.warnf("Executor thread pool no longer exhausted, request processing continues after %s discarded request(s)", rejectedRequests.sumThenReset()); - } - } - } - } catch (RejectedExecutionException e) { - if (!loadSheddingActive) { - synchronized (rejectedRequests) { - if (!loadSheddingActive) { - loadSheddingActive = true; - logger.warn("Executor thread pool exhausted, starting to reject requests"); - } - } - } - rejectedRequests.increment(); - // if the thread pool has been configured with a maximum queue size, it might reject the request - context.fail(HttpStatus.SC_SERVICE_UNAVAILABLE); - } - } - - private boolean ignoreContext(RoutingContext context) { - return contextFilter != null && contextFilter.test(context); - } - - private void runBlockingCode(RoutingContext context) { - KeycloakSession session = configureContextualData(context); - - try { - context.next(); - } catch (Throwable cause) { - // re-throw so that the any exception is handled from parent - throw new RuntimeException(cause); - } finally { - // force closing the session if not already closed - // under some circumstances resteasy might not be invoked like when no route is found for a particular path - // in this case context is set with status code 404, and we need to close the session - close(session); - } - } - - private KeycloakSession configureContextualData(RoutingContext context) { - KeycloakSession session = create(); - - Resteasy.pushContext(KeycloakSession.class, session); - context.put(KeycloakSession.class.getName(), session); - - ClientConnection connection = createClientConnection(context.request()); - - Resteasy.pushContext(ClientConnection.class, connection); - context.put(ClientConnection.class.getName(), connection); - - return session; - } - - private ClientConnection createClientConnection(HttpServerRequest request) { - return new ClientConnection() { - @Override - public String getRemoteAddr() { - return request.remoteAddress().host(); - } - - @Override - public String getRemoteHost() { - return request.remoteAddress().host(); - } - - @Override - public int getRemotePort() { - return request.remoteAddress().port(); - } - - @Override - public String getLocalAddr() { - return request.localAddress().host(); - } - - @Override - public int getLocalPort() { - return request.localAddress().port(); - } - }; - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/DebugHostnameSettingsResource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/DebugHostnameSettingsResource.java index f2cc5de612..41bd2e25fb 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/DebugHostnameSettingsResource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/resources/DebugHostnameSettingsResource.java @@ -16,6 +16,8 @@ */ package org.keycloak.quarkus.runtime.services.resources; +import io.quarkus.resteasy.reactive.server.EndpointDisabled; +import jakarta.ws.rs.NotFoundException; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.quarkus.runtime.Environment; @@ -43,6 +45,7 @@ import java.util.Map; import java.util.TreeMap; @Path("/realms") +@EndpointDisabled(name = "kc.hostname-debug", stringValue = "false", disableIfMissing = true) public class DebugHostnameSettingsResource { public static final String DEFAULT_PATH_SUFFIX = "hostname-debug"; public static final String PATH_FOR_TEST_CORS_IN_HEADERS = "test"; @@ -66,9 +69,14 @@ public class DebugHostnameSettingsResource { @Path("/{realmName}/" + DEFAULT_PATH_SUFFIX) @Produces(MediaType.TEXT_HTML) public String debug(final @PathParam("realmName") String realmName) throws IOException, FreeMarkerException { - FreeMarkerProvider freeMarkerProvider = keycloakSession.getProvider(FreeMarkerProvider.class); RealmModel realmModel = keycloakSession.realms().getRealmByName(realmName); + if (realmModel == null) { + throw new NotFoundException(); + } + + FreeMarkerProvider freeMarkerProvider = keycloakSession.getProvider(FreeMarkerProvider.class); + URI frontendUri = keycloakSession.getContext().getUri(UrlType.FRONTEND).getBaseUri(); URI backendUri = keycloakSession.getContext().getUri(UrlType.BACKEND).getBaseUri(); URI adminUri = keycloakSession.getContext().getUri(UrlType.ADMIN).getBaseUri(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/TransactionalSessionHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/TransactionalSessionHandler.java index 348657d572..00945eeef1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/TransactionalSessionHandler.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/TransactionalSessionHandler.java @@ -17,11 +17,10 @@ package org.keycloak.quarkus.runtime.transaction; -import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory; - import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakTransactionManager; +import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import org.keycloak.services.DefaultKeycloakSession; /** @@ -37,7 +36,7 @@ public interface TransactionalSessionHandler { * @return a transactional keycloak session */ default KeycloakSession create() { - KeycloakSessionFactory sessionFactory = getSessionFactory(); + KeycloakSessionFactory sessionFactory = QuarkusKeycloakSessionFactory.getInstance(); KeycloakSession session = sessionFactory.create(); session.getTransactionManager().begin(); return session; diff --git a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider b/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider index 6d1dceba46..4eb7a3f459 100644 --- a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider +++ b/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider @@ -1 +1 @@ -org.keycloak.quarkus.runtime.integration.jaxrs.ResteasyVertxProvider \ No newline at end of file +org.keycloak.quarkus.runtime.integration.resteasy.ResteasyVertxProvider \ No newline at end of file diff --git a/quarkus/runtime/src/main/resources/application.properties b/quarkus/runtime/src/main/resources/application.properties index 0822af1856..dfb12b4f6b 100644 --- a/quarkus/runtime/src/main/resources/application.properties +++ b/quarkus/runtime/src/main/resources/application.properties @@ -44,6 +44,13 @@ quarkus.log.category."io.quarkus.config".level=off quarkus.log.category."io.quarkus.arc.processor.BeanArchives".level=off quarkus.log.category."io.quarkus.arc.processor.IndexClassLookupUtils".level=off quarkus.log.category."io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor".level=warn +quarkus.log.category."io.quarkus.deployment.steps.ReflectiveHierarchyStep".level=error # Set default directory name for the location of the transaction logs -quarkus.transaction-manager.object-store.directory=${kc.home.dir:default}${file.separator}data${file.separator}transaction-logs \ No newline at end of file +quarkus.transaction-manager.object-store.directory=${kc.home.dir:default}${file.separator}data${file.separator}transaction-logs + +# Sets the minimum size for a form attribute +quarkus.http.limits.max-form-attribute-size=32768 + +# Configure the content-types that should be recognized as file parts when processing multipart form requests +quarkus.http.body.multipart.file-content-types=application/octet-stream \ No newline at end of file diff --git a/quarkus/server/pom.xml b/quarkus/server/pom.xml index 53c1253993..4cd6f2f884 100644 --- a/quarkus/server/pom.xml +++ b/quarkus/server/pom.xml @@ -19,7 +19,14 @@ org.keycloak keycloak-quarkus-server - + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + diff --git a/quarkus/tests/integration/pom.xml b/quarkus/tests/integration/pom.xml index 52f5a8484a..7919b7582c 100644 --- a/quarkus/tests/integration/pom.xml +++ b/quarkus/tests/integration/pom.xml @@ -71,6 +71,10 @@ bctls-fips test + + com.fasterxml.jackson.core + jackson-databind + diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HostnameDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HostnameDistTest.java index edf2a916cd..9c21543cbd 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HostnameDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HostnameDistTest.java @@ -19,6 +19,7 @@ package org.keycloak.it.cli.dist; import io.quarkus.test.junit.main.Launch; import io.restassured.RestAssured; +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -111,60 +112,55 @@ public class HostnameDistTest { @WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" }) @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-port=8543" }) public void testWelcomePageAdminUrl() { - Assert.assertTrue(when().get("http://mykeycloak.org:8080").asString().contains("http://mykeycloak.org:8080/admin/")); - Assert.assertTrue(when().get("https://mykeycloak.org:8443").asString().contains("https://mykeycloak.org:8443/admin/")); - Assert.assertTrue(when().get("http://localhost:8080").asString().contains("http://localhost:8080/admin/")); - Assert.assertTrue(when().get("https://localhost:8443").asString().contains("https://localhost:8443/admin/")); + when().get("http://mykeycloak.org:8080").then().body(Matchers.containsString("http://mykeycloak.org:8080/admin/")); + when().get("https://mykeycloak.org:8443").then().body(Matchers.containsString("https://mykeycloak.org:8443/admin/")); + when().get("http://localhost:8080").then().body(Matchers.containsString("http://localhost:8080/admin/")); + when().get("https://localhost:8443").then().body(Matchers.containsString("https://localhost:8443/admin/")); } @Test @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=true" }) public void testDebugHostnameSettingsEnabled() { - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 200); - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Configuration property")); - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Server mode")); - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("production [start]")); + when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(200); + when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Configuration property")); + when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Server mode")); + when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("production [start]")); - Assert.assertTrue( - when().get("http://mykeycloak.org:8080/realms/master/" + + when().get("http://mykeycloak.org:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX + - "/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS) - .getStatusCode() == 200 - ); - Assert.assertTrue( - when().get("http://localhost:8080/realms/master/" + - DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX + - "/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS) - .asString() - .contains(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK") - ); + "/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + ).then().statusCode(200); + when().get("http://localhost:8080/realms/master/" + + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX + + "/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS) + .then() + .body(Matchers.containsString(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK")); + when().get("http://localhost:8080/realms/non-existent/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404); } @Test @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=false" }) public void testDebugHostnameSettingsDisabledBySetting() { - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404); - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404")); + when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404); } @Test @Launch({ "start", "--hostname=mykeycloak.org"}) public void testDebugHostnameSettingsDisabledByDefault() { - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404); - Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404")); + when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404); } @Test @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-admin=mykeycloakadmin.org" }) public void testHostnameAdminSet() { - Assert.assertTrue(when().get("https://mykeycloak.org:8443/admin/master/console").asString().contains("\"authUrl\": \"https://mykeycloakadmin.org:8443\"")); - Assert.assertTrue(when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.org:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").asString().contains("Sign in to your account")); + when().get("https://mykeycloak.org:8443/admin/master/console").then().body(Matchers.containsString("\"authUrl\": \"https://mykeycloakadmin.org:8443\"")); + when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.org:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Sign in to your account")); } @Test @Launch({ "start", "--hostname=mykeycloak.org" }) public void testInvalidRedirectUriWhenAdminNotSet() { - Assert.assertTrue(when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.127.0.0.1.nip.io:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").asString().contains("Invalid parameter: redirect_uri")); + when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.127.0.0.1.nip.io:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Invalid parameter: redirect_uri")); } @Test @@ -177,7 +173,7 @@ public class HostnameDistTest { @WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" }) @Launch({ "start", "--proxy=edge", "--hostname=mykeycloak.org", "--hostname-admin-url=http://mykeycloakadmin.org:1234" }) public void testAdminUrl() { - Assert.assertTrue(when().get("https://mykeycloak.org:8443").asString().contains("http://mykeycloakadmin.org:1234/admin/")); + when().get("https://mykeycloak.org:8443").then().body(Matchers.containsString("http://mykeycloakadmin.org:1234/admin/")); } @Test diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java index da84d6eed0..34909229dd 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java @@ -51,7 +51,7 @@ public class LoggingDistTest { @Launch({ "start-dev", "--log-level=debug" }) void testSetRootLevel(LaunchResult result) { CLIResult cliResult = (CLIResult) result; - assertTrue(cliResult.getOutput().contains("DEBUG [io.quarkus.resteasy.runtime]")); + assertTrue(cliResult.getOutput().contains("DEBUG [io.netty.util.internal")); cliResult.assertStartedDevMode(); } @@ -76,7 +76,7 @@ public class LoggingDistTest { @Launch({ "start-dev", "--log-level=off,org.keycloak:warn,debug" }) void testSetLastRootLevelIfMultipleSet(LaunchResult result) { CLIResult cliResult = (CLIResult) result; - assertTrue(cliResult.getOutput().contains("DEBUG [io.quarkus.resteasy.runtime]")); + assertTrue(cliResult.getOutput().contains("DEBUG [io.netty.util.internal")); assertFalse(cliResult.getOutput().contains("INFO [org.keycloak")); cliResult.assertStartedDevMode(); } @@ -85,7 +85,7 @@ public class LoggingDistTest { @Launch({ "start-dev", "--log-level=\"off,org.keycloak:warn,debug\"" }) void testWinSetLastRootLevelIfMultipleSet(LaunchResult result) { CLIResult cliResult = (CLIResult) result; - assertTrue(cliResult.getOutput().contains("DEBUG [io.quarkus.resteasy.runtime]")); + assertTrue(cliResult.getOutput().contains("DEBUG [io.netty.util.internal")); assertFalse(cliResult.getOutput().contains("INFO [org.keycloak")); cliResult.assertStartedDevMode(); } diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java b/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java index 186ffd59e7..3e9b0dd15f 100644 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java @@ -16,6 +16,9 @@ */ package org.keycloak.models; +import static org.keycloak.common.util.UriUtils.parseQueryParameters; + +import jakarta.ws.rs.core.MultivaluedHashMap; import org.jboss.resteasy.spi.ResteasyUriBuilder; import org.keycloak.urls.HostnameProvider; import org.keycloak.urls.UrlType; @@ -26,6 +29,7 @@ import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; import java.net.URI; import java.util.List; +import java.util.Map; public class KeycloakUriInfo implements UriInfo { @@ -153,7 +157,22 @@ public class KeycloakUriInfo implements UriInfo { @Override public MultivaluedMap getQueryParameters(boolean decode) { - return delegate.getQueryParameters(decode); + if (decode) { + return delegate.getQueryParameters(decode); + } + + MultivaluedMap result = new MultivaluedHashMap<>(); + String rawQuery = delegate.getRequestUri().getRawQuery(); + + if (rawQuery == null) { + return result; + } + + for (Map.Entry> entry : parseQueryParameters(rawQuery, false).entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } + + return result; } @Override diff --git a/services/src/main/java/org/keycloak/platform/PlatformProvider.java b/services/src/main/java/org/keycloak/platform/PlatformProvider.java index 159503c818..f28b4789f7 100644 --- a/services/src/main/java/org/keycloak/platform/PlatformProvider.java +++ b/services/src/main/java/org/keycloak/platform/PlatformProvider.java @@ -25,9 +25,13 @@ public interface PlatformProvider { String name(); - void onStartup(Runnable runnable); + default void onStartup(Runnable runnable) { - void onShutdown(Runnable runnable); + } + + default void onShutdown(Runnable runnable) { + + } void exit(Throwable cause); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java index a1fc106f9d..f650d7666f 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java @@ -47,7 +47,7 @@ public class DefaultKeycloakContext implements KeycloakContext { private ClientModel client; - private KeycloakSession session; + protected KeycloakSession session; private Map uriInfo; @@ -178,4 +178,8 @@ public class DefaultKeycloakContext implements KeycloakContext { protected HttpResponse createHttpResponse() { return new HttpResponseImpl(session, getContextObject(org.jboss.resteasy.spi.HttpResponse.class)); } + + protected KeycloakSession getSession() { + return session; + } } diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java index 4d76aba3a1..f9d9de9d10 100644 --- a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java +++ b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java @@ -1,14 +1,21 @@ package org.keycloak.services.error; +import static org.keycloak.common.util.Resteasy.getContextData; +import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory; + +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.Failure; import org.keycloak.Config; +import org.keycloak.OAuthErrorException; import org.keycloak.forms.login.freemarker.model.UrlBean; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionTaskWithResult; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.messages.Messages; @@ -22,7 +29,6 @@ import org.keycloak.utils.MediaType; import org.keycloak.utils.MediaTypeMatcher; import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; @@ -43,11 +49,21 @@ public class KeycloakErrorHandler implements ExceptionMapper { public static final String UNCAUGHT_SERVER_ERROR_TEXT = "Uncaught server error"; public static final String ERROR_RESPONSE_TEXT = "Error response {0}"; - @Context - KeycloakSession session; - @Override public Response toResponse(Throwable throwable) { + KeycloakSession session = getContextData(KeycloakSession.class); + + if (session == null) { + // errors might be thrown when handling errors from JAX-RS before the session is available + return KeycloakModelUtils.runJobInTransactionWithResult(getSessionFactory(), + new KeycloakSessionTaskWithResult() { + @Override + public Response run(KeycloakSession session) { + return getResponse(session, throwable); + } + }); + } + return getResponse(session, throwable); } @@ -118,6 +134,12 @@ public class KeycloakErrorHandler implements ExceptionMapper { } private static String getErrorCode(Throwable throwable) { + Throwable cause = throwable.getCause(); + + if (cause instanceof JsonParseException) { + return OAuthErrorException.INVALID_REQUEST; + } + if (throwable instanceof WebApplicationException && throwable.getMessage() != null) { return throwable.getMessage(); } diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakMismatchedInputExceptionHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakMismatchedInputExceptionHandler.java new file mode 100644 index 0000000000..029da13f61 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/error/KeycloakMismatchedInputExceptionHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 + * Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.services.error; + +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import org.keycloak.models.KeycloakSession; + +/** + * Override explicitly added ExceptionMapper for handling {@link MismatchedInputException} in RestEasy Jackson + */ +public class KeycloakMismatchedInputExceptionHandler implements ExceptionMapper { + + @Context + KeycloakSession session; + + /** + * Return escaped original message + */ + @Override + public Response toResponse(MismatchedInputException exception) { + return KeycloakErrorHandler.getResponse(session, exception); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index aae1726bce..5f22483355 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -45,6 +45,7 @@ import org.keycloak.services.DefaultKeycloakSessionFactory; import org.keycloak.services.ServicesLogger; import org.keycloak.services.error.KeycloakErrorHandler; import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler; +import org.keycloak.services.error.KeycloakMismatchedInputExceptionHandler; import org.keycloak.services.filters.KeycloakSecurityHeadersFilter; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.RealmManager; @@ -90,14 +91,13 @@ public class KeycloakApplication extends Application { logger.debugv("PlatformProvider: {0}", platform.getClass().getName()); logger.debugv("RestEasy provider: {0}", Resteasy.getProvider().getClass().getName()); - CryptoIntegration.init(KeycloakApplication.class.getClassLoader()); loadConfig(); - singletons.add(new RobotsResource()); - singletons.add(new RealmsResource()); + classes.add(RobotsResource.class); + classes.add(RealmsResource.class); if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_API)) { - singletons.add(new AdminRoot()); + classes.add(AdminRoot.class); } classes.add(ThemeResource.class); @@ -108,9 +108,10 @@ public class KeycloakApplication extends Application { classes.add(KeycloakSecurityHeadersFilter.class); classes.add(KeycloakErrorHandler.class); classes.add(KcUnrecognizedPropertyExceptionHandler.class); + classes.add(KeycloakMismatchedInputExceptionHandler.class); singletons.add(new ObjectMapperResolver()); - singletons.add(new WelcomeResource()); + classes.add(WelcomeResource.class); platform.onStartup(this::startup); platform.onShutdown(this::shutdown); @@ -122,6 +123,7 @@ public class KeycloakApplication extends Application { } protected void startup() { + CryptoIntegration.init(KeycloakApplication.class.getClassLoader()); KeycloakApplication.sessionFactory = createSessionFactory(); ExportImportManager[] exportImportManager = new ExportImportManager[1]; diff --git a/services/src/main/java/org/keycloak/utils/OAuth2Error.java b/services/src/main/java/org/keycloak/utils/OAuth2Error.java index b2c2dfc731..89c0d8ff6c 100644 --- a/services/src/main/java/org/keycloak/utils/OAuth2Error.java +++ b/services/src/main/java/org/keycloak/utils/OAuth2Error.java @@ -137,7 +137,7 @@ public class OAuth2Error { bearer.setErrorDescription(errorDescription); WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer); wwwAuthenticate.build(builder::header); - builder.entity(""); + builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE); } return constructor.newInstance(builder.build()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java index 3a70cc468d..2ff919bf57 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java @@ -12,6 +12,7 @@ import org.hamcrest.CoreMatchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Test; +import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.util.StreamUtil; @@ -93,7 +94,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest { assertEquals(400, response.getStatusLine().getStatusCode()); OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class); - assertEquals("unknown_error", error.getError()); + assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError()); assertNull(error.getErrorDescription()); } } @@ -113,7 +114,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest { assertEquals(400, response.getStatusLine().getStatusCode()); OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class); - assertEquals("unknown_error", error.getError()); + assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError()); assertNull(error.getErrorDescription()); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java index 9e43020a93..bc2bb3e8db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; import org.junit.Assert; @@ -33,7 +34,10 @@ import org.keycloak.admin.client.resource.UserResource; import org.keycloak.federation.kerberos.KerberosFederationProvider; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.LDAPRule; @@ -205,4 +209,40 @@ public class LDAPAdminRestApiTest extends AbstractLDAPTest { // Expected } } + + @Test + public void testErrorResponseWhenLdapIsFailing() { + // Create user just with the username + UserRepresentation user1 = UserBuilder.create() + .username("admintestuser1") + .password("userpass") + .enabled(true) + .build(); + String newUserId1 = createUserExpectSuccess(user1); + getCleanup().addUserId(newUserId1); + + List storageProviders = testRealm().components().query(testRealm().toRepresentation().getId(), UserStorageProvider.class.getName()); + ComponentRepresentation ldapProvider = storageProviders.get(0); + List originalUrl = ldapProvider.getConfig().get(LDAPConstants.CONNECTION_URL); + + getCleanup().addCleanup(new AutoCloseable() { + @Override + public void close() { + ldapProvider.getConfig().put(LDAPConstants.CONNECTION_URL, originalUrl); + testRealm().components().component(ldapProvider.getId()).update(ldapProvider); + } + }); + + ldapProvider.getConfig().put(LDAPConstants.CONNECTION_URL, List.of("ldap://invalid")); + testRealm().components().component(ldapProvider.getId()).update(ldapProvider); + + try { + List search = testRealm().users().search("*", -1, -1, true); + Assert.fail("Should fail because LDAP is in failing state"); + } catch (WebApplicationException expected) { + Response response = expected.getResponse(); + OAuth2ErrorRepresentation error = response.readEntity(OAuth2ErrorRepresentation.class); + assertEquals("unknown_error", error.getError()); + } + } }