Switch to Resteasy Reactive

Closes #10713
This commit is contained in:
Pedro Igor 2023-08-14 10:11:43 -03:00
parent 2ccb6871e4
commit 217a09ce46
47 changed files with 1083 additions and 563 deletions

View file

@ -58,7 +58,7 @@ public class UriUtils {
} }
} }
public static MultivaluedHashMap<String, String> decodeQueryString(String queryString) { public static MultivaluedHashMap<String, String> parseQueryParameters(String queryString, boolean decode) {
MultivaluedHashMap<String, String> map = new MultivaluedHashMap<String, String>(); MultivaluedHashMap<String, String> map = new MultivaluedHashMap<String, String>();
if (queryString == null || queryString.equals("")) return map; if (queryString == null || queryString.equals("")) return map;
@ -71,9 +71,9 @@ public class UriUtils {
String[] nv = param.split("=", 2); String[] nv = param.split("=", 2);
try 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] : ""; 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) catch (UnsupportedEncodingException e)
{ {
@ -84,7 +84,7 @@ public class UriUtils {
{ {
try try
{ {
String name = URLDecoder.decode(param, "UTF-8"); String name = decode ? URLDecoder.decode(param, "UTF-8") : param;
map.add(name, ""); map.add(name, "");
} }
catch (UnsupportedEncodingException e) catch (UnsupportedEncodingException e)
@ -96,6 +96,10 @@ public class UriUtils {
return map; return map;
} }
public static MultivaluedHashMap<String, String> decodeQueryString(String queryString) {
return parseQueryParameters(queryString, true);
}
public static String stripQueryParam(String url, String name){ public static String stripQueryParam(String url, String name){
return url.replaceFirst("[\\?&]"+name+"=[^&]*$|"+name+"=[^&]*&", ""); return url.replaceFirst("[\\?&]"+name+"=[^&]*$|"+name+"=[^&]*&", "");
} }

View file

@ -49,11 +49,11 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-deployment</artifactId> <artifactId>quarkus-resteasy-reactive-deployment</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson-deployment</artifactId> <artifactId>quarkus-resteasy-reactive-jackson-deployment</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>

View file

@ -40,11 +40,9 @@ import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig;
import io.quarkus.hibernate.orm.deployment.PersistenceXmlDescriptorBuildItem; import io.quarkus.hibernate.orm.deployment.PersistenceXmlDescriptorBuildItem;
import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem; import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem;
import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem; 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.ConfigurationException;
import io.quarkus.runtime.configuration.ProfileManager; 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.quarkus.vertx.http.deployment.RouteBuildItem;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
@ -55,8 +53,10 @@ import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName; import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView; import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger; 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.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorSpi; 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.QuarkusPropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; 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.integration.web.NotFoundHandler;
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck; import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory; 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.ScriptProviderDescriptor;
import org.keycloak.representations.provider.ScriptProviderMetadata; import org.keycloak.representations.provider.ScriptProviderMetadata;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.theme.ClasspathThemeProviderFactory; import org.keycloak.theme.ClasspathThemeProviderFactory;
import org.keycloak.theme.ClasspathThemeResourceProviderFactory; import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
import org.keycloak.theme.FolderThemeProviderFactory; import org.keycloak.theme.FolderThemeProviderFactory;
@ -181,10 +182,6 @@ class KeycloakProcessor {
ClasspathThemeResourceProviderFactory.class, ClasspathThemeResourceProviderFactory.class,
JarThemeProviderFactory.class, JarThemeProviderFactory.class,
JpaMapStorageProviderFactory.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 { static {
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator); DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
@ -593,27 +590,6 @@ class KeycloakProcessor {
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-jpa")); indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-jpa"));
} }
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void initializeFilter(BuildProducer<FilterBuildItem> filters, KeycloakRecorder recorder, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
List<String> 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 @BuildStep
void disableMetricsEndpoint(BuildProducer<RouteBuildItem> routes) { void disableMetricsEndpoint(BuildProducer<RouteBuildItem> routes) {
if (!isMetricsEnabled()) { if (!isMetricsEnabled()) {
@ -641,15 +617,19 @@ class KeycloakProcessor {
} }
@BuildStep @BuildStep
void configureResteasy(BuildProducer<ResteasyDeploymentCustomizerBuildItem> deploymentCustomizerProducer) { void configureResteasy(CombinedIndexBuildItem index,
deploymentCustomizerProducer.produce(new ResteasyDeploymentCustomizerBuildItem(new Consumer<ResteasyDeployment>() { BuildProducer<BuildTimeConditionBuildItem> buildTimeConditionBuildItemBuildProducer,
BuildProducer<MethodScannerBuildItem> 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 @Override
public void accept(ResteasyDeployment resteasyDeployment) { public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
// we need to explicitly set the application to avoid errors at build time due to the application Map<String, Object> methodContext) {
// from keycloak-services also being added to the index return List.of(chainCustomizer);
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);
} }
})); }));
} }

View file

@ -41,11 +41,11 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId> <artifactId>quarkus-resteasy-reactive</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId> <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>

View file

@ -42,7 +42,6 @@ import org.keycloak.common.crypto.FipsMode;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; 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.quarkus.runtime.storage.database.liquibase.FastServiceLocator;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
@ -141,29 +140,6 @@ public class KeycloakRecorder {
}; };
} }
public QuarkusRequestFilter createRequestFilter(List<String> ignoredPaths) {
return new QuarkusRequestFilter(createIgnoredHttpPathsPredicate(ignoredPaths));
}
private Predicate<RoutingContext> createIgnoredHttpPathsPredicate(List<String> 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) { public void setCryptoProvider(FipsMode fipsMode) {
String cryptoProvider = fipsMode.getProviderClassName(); String cryptoProvider = fipsMode.getProviderClassName();

View file

@ -107,6 +107,16 @@ public final class Configuration {
return getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName)); return getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName));
} }
public static Optional<Boolean> getOptionalBooleanKcValue(String propertyName) {
Optional<String> value = getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName));
if (value.isPresent()) {
return value.map(Boolean::parseBoolean);
}
return Optional.empty();
}
public static Optional<Boolean> getOptionalBooleanValue(String name) { public static Optional<Boolean> getOptionalBooleanValue(String name) {
return getOptionalValue(name).map(Boolean::parseBoolean); return getOptionalValue(name).map(Boolean::parseBoolean);
} }

View file

@ -174,6 +174,11 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
protected URI getRealmFrontEndUrl() { protected URI getRealmFrontEndUrl() {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
if (session == null) {
return null;
}
RealmModel realm = session.getContext().getRealm(); RealmModel realm = session.getContext().getRealm();
if (realm == null) { if (realm == null) {
@ -300,8 +305,7 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
} }
private int getRequestPort(UriInfo uriInfo) { private int getRequestPort(UriInfo uriInfo) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); return uriInfo.getBaseUri().getPort();
return session.getContext().getHttpRequest().getUri().getBaseUri().getPort();
} }
private <T> T fromBaseUriOrDefault(Function<URI, T> resolver, URI baseUri, T defaultValue) { private <T> T fromBaseUriOrDefault(Function<URI, T> resolver, URI baseUri, T defaultValue) {

View file

@ -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 <R> QuarkusHttpRequest(HttpRequest delegate) {
super(delegate);
}
@Override
public X509Certificate[] getClientCertificateChain() {
Instance<RoutingContext> 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;
}
}

View file

@ -18,11 +18,12 @@
package org.keycloak.quarkus.runtime.integration; package org.keycloak.quarkus.runtime.integration;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.integration.resteasy.QuarkusKeycloakContext;
import org.keycloak.services.DefaultKeycloakContext; import org.keycloak.services.DefaultKeycloakContext;
import org.keycloak.services.DefaultKeycloakSession; import org.keycloak.services.DefaultKeycloakSession;
import org.keycloak.services.DefaultKeycloakSessionFactory; import org.keycloak.services.DefaultKeycloakSessionFactory;
public class QuarkusKeycloakSession extends DefaultKeycloakSession { public final class QuarkusKeycloakSession extends DefaultKeycloakSession {
public QuarkusKeycloakSession(DefaultKeycloakSessionFactory factory) { public QuarkusKeycloakSession(DefaultKeycloakSessionFactory factory) {
super(factory); super(factory);

View file

@ -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();
}
}

View file

@ -77,23 +77,10 @@ public class QuarkusPlatform implements PlatformProvider {
} }
} }
Runnable startupHook;
Runnable shutdownHook;
private AtomicBoolean started = new AtomicBoolean(false); private AtomicBoolean started = new AtomicBoolean(false);
private List<Throwable> deferredExceptions = new CopyOnWriteArrayList<>(); private List<Throwable> deferredExceptions = new CopyOnWriteArrayList<>();
private File tmpDir; private File tmpDir;
@Override
public void onStartup(Runnable startupHook) {
this.startupHook = startupHook;
}
@Override
public void onShutdown(Runnable shutdownHook) {
this.shutdownHook = shutdownHook;
}
@Override @Override
public void exit(Throwable cause) { public void exit(Throwable cause) {
Quarkus.asyncExit(1); Quarkus.asyncExit(1);
@ -102,7 +89,7 @@ public class QuarkusPlatform implements PlatformProvider {
/** /**
* Called when Quarkus platform is started * Called when Quarkus platform is started
*/ */
void started() { public void started() {
this.started.set(true); this.started.set(true);
} }

View file

@ -15,21 +15,23 @@
* limitations under the License. * 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.common.util.Resteasy;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.services.DefaultKeycloakContext;
public class QuarkusKeycloakContext extends DefaultKeycloakContext { import io.quarkus.arc.Unremovable;
public QuarkusKeycloakContext(KeycloakSession session) { @ApplicationScoped
super(session); @Unremovable
} public class KeycloakBeanProducer {
@Override @Produces
protected HttpRequest createHttpRequest() { @RequestScoped
return new QuarkusHttpRequest(Resteasy.getContextData(org.jboss.resteasy.spi.HttpRequest.class)); public KeycloakSession getKeycloakSession() {
return Resteasy.getContextData(KeycloakSession.class);
} }
} }

View file

@ -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;
/**
* <p>This {@link ContextInjector} allows injecting {@link ClientConnection} to JAX-RS resources.
*
* <p>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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Provider
public class ClientConnectionContextInjector implements ContextInjector<ClientConnection, ClientConnection> {
@Override
public ClientConnection resolve(Class rawType, Type genericType, Annotation[] annotations) {
return Resteasy.getContextData(ClientConnection.class);
}
}

View file

@ -18,7 +18,10 @@
package org.keycloak.quarkus.runtime.integration.jaxrs; package org.keycloak.quarkus.runtime.integration.jaxrs;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.util.stream.Stream; 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.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.container.ContainerResponseFilter;
@ -29,28 +32,40 @@ import org.keycloak.common.util.Resteasy;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler; import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler;
import jakarta.annotation.Priority;
@Provider @Provider
@PreMatching @PreMatching
@Priority(1) @Priority(1)
public class TransactionalResponseFilter implements ContainerResponseFilter, TransactionalSessionHandler { public class CloseSessionHandler implements ContainerResponseFilter, TransactionalSessionHandler {
@Override @Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException { throws IOException {
Object entity = responseContext.getEntity(); Object entity = responseContext.getEntity();
if (shouldDelaySessionClose(entity)) { if (entity instanceof Stream) {
Stream entityStream = (Stream) entity;
entityStream.onClose(this::closeSession);
return; 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;
} }
private static boolean shouldDelaySessionClose(Object entity) { closeSession();
// 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));
} }
} }

View file

@ -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<K, V> implements MultivaluedMap<K, V> {
@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<V> valueList) {
throw new UnsupportedOperationException();
}
@Override
public void addFirst(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public boolean equalsIgnoreValueOrder(MultivaluedMap<K, V> 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<V> get(Object key) {
return null;
}
@Override
public List<V> put(K key, List<V> value) {
throw new UnsupportedOperationException();
}
@Override
public List<V> remove(Object key) {
throw new UnsupportedOperationException();
}
@Override
public void putAll(Map<? extends K, ? extends List<V>> m) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public Set<K> keySet() {
return emptySet();
}
@Override
public Collection<List<V>> values() {
return emptySet();
}
@Override
public Set<Entry<K, List<V>>> entrySet() {
return emptySet();
}
}

View file

@ -24,7 +24,7 @@ import java.lang.reflect.Type;
import org.jboss.resteasy.spi.ContextInjector; import org.jboss.resteasy.spi.ContextInjector;
import org.keycloak.common.util.Resteasy; import org.keycloak.common.util.Resteasy;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter; import org.keycloak.quarkus.runtime.integration.resteasy.ResteasyVertxProvider;
/** /**
* <p>This {@link ContextInjector} allows injecting {@link KeycloakSession} to JAX-RS resources. * <p>This {@link ContextInjector} allows injecting {@link KeycloakSession} to JAX-RS resources.
@ -32,13 +32,12 @@ import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter;
* <p>Due to the latest changes in Quarkus, the context map is cleared prior to dispatching to JAX-RS resources, so we need * <p>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. * to delegate to the {@link ResteasyVertxProvider} provider the lookup of Keycloak contextual objects.
* *
* @see QuarkusRequestFilter
* @see ResteasyVertxProvider * @see ResteasyVertxProvider
* *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/ */
@Provider @Provider
public class KeycloakContextInjector implements ContextInjector<KeycloakSession, KeycloakSession> { public class KeycloakSessionContextInjector implements ContextInjector<KeycloakSession, KeycloakSession> {
@Override @Override
public KeycloakSession resolve(Class rawType, Type genericType, Annotation[] annotations) { public KeycloakSession resolve(Class rawType, Type genericType, Annotation[] annotations) {
return Resteasy.getContextData(KeycloakSession.class); return Resteasy.getContextData(KeycloakSession.class);

View file

@ -17,26 +17,40 @@
package org.keycloak.quarkus.runtime.integration.jaxrs; package org.keycloak.quarkus.runtime.integration.jaxrs;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import jakarta.enterprise.event.Observes;
import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.ApplicationPath;
import org.keycloak.config.HostnameOptions; import org.keycloak.config.HostnameOptions;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.platform.Platform;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; 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.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource; import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.WelcomeResource; import org.keycloak.services.resources.WelcomeResource;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.common.annotation.Blocking;
@ApplicationPath("/") @ApplicationPath("/")
@Blocking
public class QuarkusKeycloakApplication extends KeycloakApplication { public class QuarkusKeycloakApplication extends KeycloakApplication {
private static boolean filterSingletons(Object o) { void onStartupEvent(@Observes StartupEvent event) {
return !WelcomeResource.class.isInstance(o); QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform();
platform.started();
QuarkusPlatform.exitOnError();
startup();
}
void onShutdownEvent(@Observes ShutdownEvent event) {
shutdown();
} }
@Override @Override
@ -53,16 +67,21 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
@Override @Override
public Set<Object> getSingletons() { public Set<Object> getSingletons() {
Set<Object> singletons = super.getSingletons().stream() return Set.of();
.filter(QuarkusKeycloakApplication::filterSingletons)
.collect(Collectors.toSet());
singletons.add(new QuarkusWelcomeResource());
if (Configuration.getOptionalBooleanValue("--" + HostnameOptions.HOSTNAME_DEBUG.getKey()).orElse(Boolean.FALSE)) {
singletons.add(new DebugHostnameSettingsResource());
} }
return singletons; @Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<>(super.getClasses());
classes.remove(WelcomeResource.class);
classes.add(QuarkusWelcomeResource.class);
classes.add(QuarkusObjectMapperResolver.class);
classes.add(CloseSessionHandler.class);
classes.add(DebugHostnameSettingsResource.class);
return classes;
} }
} }

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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<Executor>() {
@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<ServerRestHandler> handlers(Phase phase, ResourceClass resourceClass,
ServerResourceMethod resourceMethod) {
List<ServerRestHandler> 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;
}
}

View file

@ -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();
}
}

View file

@ -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<String, String> EMPTY_FORM_PARAM = new EmptyMultivaluedMap<>();
private static final MultivaluedMap<String, FormPartValue> EMPTY_MULTI_MAP_MULTI_PART = new EmptyMultivaluedMap<>();
private final ResteasyReactiveRequestContext context;
public <R> QuarkusHttpRequest(ResteasyReactiveRequestContext context) {
this.context = context;
}
@Override
public String getHttpMethod() {
return context.getMethod();
}
@Override
public MultivaluedMap<String, String> getDecodedFormParameters() {
FormData parameters = context.getFormData();
if (parameters == null || !parameters.iterator().hasNext()) {
return EMPTY_FORM_PARAM;
}
MultivaluedMap<String, String> params = new QuarkusMultivaluedHashMap<>();
for (String name : parameters) {
Deque<FormValue> values = parameters.get(name);
if (values == null || values.isEmpty()) {
continue;
}
for (FormValue value : values) {
params.add(name, value.getValue());
}
}
return params;
}
@Override
public MultivaluedMap<String, FormPartValue> getMultiPartFormParameters() {
FormData formData = context.getFormData();
if (formData == null) {
return EMPTY_MULTI_MAP_MULTI_PART;
}
MultivaluedMap<String, FormPartValue> params = new QuarkusMultivaluedHashMap<>();
for (String name : formData) {
Deque<FormValue> formValues = formData.get(name);
if (formValues != null) {
Iterator<FormValue> 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<RoutingContext> 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();
}
}

View file

@ -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<HttpCookie> 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());
}
}
}

View file

@ -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();
}
}

View file

@ -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. * and other contributors as indicated by the @author tags.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -15,15 +15,14 @@
* limitations under the License. * 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 io.vertx.ext.web.RoutingContext;
import org.jboss.resteasy.core.ResteasyContext; import org.jboss.resteasy.core.ResteasyContext;
import org.keycloak.common.util.ResteasyProvider; 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 { public class ResteasyVertxProvider implements ResteasyProvider {
@Override @Override
@ -31,7 +30,7 @@ public class ResteasyVertxProvider implements ResteasyProvider {
R data = ResteasyContext.getContextData(type); R data = ResteasyContext.getContextData(type);
if (data == null) { if (data == null) {
RoutingContext contextData = ResteasyContext.getContextData(RoutingContext.class); RoutingContext contextData = Arc.container().instance(CurrentVertxRequest.class).get().getCurrent();
if (contextData == null) { if (contextData == null) {
return null; return null;
@ -44,14 +43,13 @@ public class ResteasyVertxProvider implements ResteasyProvider {
} }
@Override @Override
public void pushDefaultContextObject(Class type, Object instance) { public void pushContext(Class type, Object instance) {
ResteasyContext.getContextData(org.jboss.resteasy.spi.Dispatcher.class).getDefaultContextObjects() ResteasyContext.pushContext(type, instance);
.put(type, instance);
} }
@Override @Override
public void pushContext(Class type, Object instance) { public void pushDefaultContextObject(Class type, Object instance) {
ResteasyContext.pushContext(type, instance); pushContext(type, instance);
} }
@Override @Override

View file

@ -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;
/**
* <p>A {@link ServerRestHandler} that set the media type produced by a JAX-RS resource method.
*
* <p>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);
}
}

View file

@ -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;
/**
* <p>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.
*
* <p>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).
*
* <p>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<RoutingContext>, TransactionalSessionHandler {
private final Logger logger = Logger.getLogger(QuarkusRequestFilter.class);
private final Predicate<RoutingContext> contextFilter;
public QuarkusRequestFilter() {
this(null);
}
public QuarkusRequestFilter(Predicate<RoutingContext> 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();
}
};
}
}

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.quarkus.runtime.services.resources; 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.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
@ -43,6 +45,7 @@ import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
@Path("/realms") @Path("/realms")
@EndpointDisabled(name = "kc.hostname-debug", stringValue = "false", disableIfMissing = true)
public class DebugHostnameSettingsResource { public class DebugHostnameSettingsResource {
public static final String DEFAULT_PATH_SUFFIX = "hostname-debug"; public static final String DEFAULT_PATH_SUFFIX = "hostname-debug";
public static final String PATH_FOR_TEST_CORS_IN_HEADERS = "test"; public static final String PATH_FOR_TEST_CORS_IN_HEADERS = "test";
@ -66,9 +69,14 @@ public class DebugHostnameSettingsResource {
@Path("/{realmName}/" + DEFAULT_PATH_SUFFIX) @Path("/{realmName}/" + DEFAULT_PATH_SUFFIX)
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
public String debug(final @PathParam("realmName") String realmName) throws IOException, FreeMarkerException { public String debug(final @PathParam("realmName") String realmName) throws IOException, FreeMarkerException {
FreeMarkerProvider freeMarkerProvider = keycloakSession.getProvider(FreeMarkerProvider.class);
RealmModel realmModel = keycloakSession.realms().getRealmByName(realmName); 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 frontendUri = keycloakSession.getContext().getUri(UrlType.FRONTEND).getBaseUri();
URI backendUri = keycloakSession.getContext().getUri(UrlType.BACKEND).getBaseUri(); URI backendUri = keycloakSession.getContext().getUri(UrlType.BACKEND).getBaseUri();
URI adminUri = keycloakSession.getContext().getUri(UrlType.ADMIN).getBaseUri(); URI adminUri = keycloakSession.getContext().getUri(UrlType.ADMIN).getBaseUri();

View file

@ -17,11 +17,10 @@
package org.keycloak.quarkus.runtime.transaction; package org.keycloak.quarkus.runtime.transaction;
import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransactionManager; import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
import org.keycloak.services.DefaultKeycloakSession; import org.keycloak.services.DefaultKeycloakSession;
/** /**
@ -37,7 +36,7 @@ public interface TransactionalSessionHandler {
* @return a transactional keycloak session * @return a transactional keycloak session
*/ */
default KeycloakSession create() { default KeycloakSession create() {
KeycloakSessionFactory sessionFactory = getSessionFactory(); KeycloakSessionFactory sessionFactory = QuarkusKeycloakSessionFactory.getInstance();
KeycloakSession session = sessionFactory.create(); KeycloakSession session = sessionFactory.create();
session.getTransactionManager().begin(); session.getTransactionManager().begin();
return session; return session;

View file

@ -1 +1 @@
org.keycloak.quarkus.runtime.integration.jaxrs.ResteasyVertxProvider org.keycloak.quarkus.runtime.integration.resteasy.ResteasyVertxProvider

View file

@ -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.BeanArchives".level=off
quarkus.log.category."io.quarkus.arc.processor.IndexClassLookupUtils".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.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 # 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 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

View file

@ -19,7 +19,14 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-quarkus-server</artifactId> <artifactId>keycloak-quarkus-server</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- Necessary for proper execution of IDELauncher --> <!-- Necessary for proper execution of IDELauncher -->
<!-- Can be removed as part of the https://github.com/keycloak/keycloak/issues/22455 enhancement --> <!-- Can be removed as part of the https://github.com/keycloak/keycloak/issues/22455 enhancement -->
<dependency> <dependency>

View file

@ -71,6 +71,10 @@
<artifactId>bctls-fips</artifactId> <artifactId>bctls-fips</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JDBC Drivers --> <!-- JDBC Drivers -->
<dependency> <dependency>

View file

@ -19,6 +19,7 @@ package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch; import io.quarkus.test.junit.main.Launch;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import org.hamcrest.Matchers;
import org.junit.Assert; import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -111,60 +112,55 @@ public class HostnameDistTest {
@WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" }) @WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" })
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-port=8543" }) @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-port=8543" })
public void testWelcomePageAdminUrl() { public void testWelcomePageAdminUrl() {
Assert.assertTrue(when().get("http://mykeycloak.org:8080").asString().contains("http://mykeycloak.org:8080/admin/")); when().get("http://mykeycloak.org:8080").then().body(Matchers.containsString("http://mykeycloak.org:8080/admin/"));
Assert.assertTrue(when().get("https://mykeycloak.org:8443").asString().contains("https://mykeycloak.org:8443/admin/")); when().get("https://mykeycloak.org:8443").then().body(Matchers.containsString("https://mykeycloak.org:8443/admin/"));
Assert.assertTrue(when().get("http://localhost:8080").asString().contains("http://localhost:8080/admin/")); when().get("http://localhost:8080").then().body(Matchers.containsString("http://localhost:8080/admin/"));
Assert.assertTrue(when().get("https://localhost:8443").asString().contains("https://localhost:8443/admin/")); when().get("https://localhost:8443").then().body(Matchers.containsString("https://localhost:8443/admin/"));
} }
@Test @Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=true" }) @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=true" })
public void testDebugHostnameSettingsEnabled() { public void testDebugHostnameSettingsEnabled() {
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 200); when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(200);
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Configuration property")); when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Configuration property"));
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Server mode")); when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("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().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.DEFAULT_PATH_SUFFIX +
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS) "/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS
.getStatusCode() == 200 ).then().statusCode(200);
);
Assert.assertTrue(
when().get("http://localhost:8080/realms/master/" + when().get("http://localhost:8080/realms/master/" +
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS) "/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
.asString() .then()
.contains(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK") .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 @Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=false" }) @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=false" })
public void testDebugHostnameSettingsDisabledBySetting() { public void testDebugHostnameSettingsDisabledBySetting() {
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404); when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404"));
} }
@Test @Test
@Launch({ "start", "--hostname=mykeycloak.org"}) @Launch({ "start", "--hostname=mykeycloak.org"})
public void testDebugHostnameSettingsDisabledByDefault() { public void testDebugHostnameSettingsDisabledByDefault() {
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404); when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404"));
} }
@Test @Test
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-admin=mykeycloakadmin.org" }) @Launch({ "start", "--hostname=mykeycloak.org", "--hostname-admin=mykeycloakadmin.org" })
public void testHostnameAdminSet() { public void testHostnameAdminSet() {
Assert.assertTrue(when().get("https://mykeycloak.org:8443/admin/master/console").asString().contains("\"authUrl\": \"https://mykeycloakadmin.org:8443\"")); when().get("https://mykeycloak.org:8443/admin/master/console").then().body(Matchers.containsString("\"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/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 @Test
@Launch({ "start", "--hostname=mykeycloak.org" }) @Launch({ "start", "--hostname=mykeycloak.org" })
public void testInvalidRedirectUriWhenAdminNotSet() { 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 @Test
@ -177,7 +173,7 @@ public class HostnameDistTest {
@WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" }) @WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" })
@Launch({ "start", "--proxy=edge", "--hostname=mykeycloak.org", "--hostname-admin-url=http://mykeycloakadmin.org:1234" }) @Launch({ "start", "--proxy=edge", "--hostname=mykeycloak.org", "--hostname-admin-url=http://mykeycloakadmin.org:1234" })
public void testAdminUrl() { 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 @Test

View file

@ -51,7 +51,7 @@ public class LoggingDistTest {
@Launch({ "start-dev", "--log-level=debug" }) @Launch({ "start-dev", "--log-level=debug" })
void testSetRootLevel(LaunchResult result) { void testSetRootLevel(LaunchResult result) {
CLIResult cliResult = (CLIResult) 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(); cliResult.assertStartedDevMode();
} }
@ -76,7 +76,7 @@ public class LoggingDistTest {
@Launch({ "start-dev", "--log-level=off,org.keycloak:warn,debug" }) @Launch({ "start-dev", "--log-level=off,org.keycloak:warn,debug" })
void testSetLastRootLevelIfMultipleSet(LaunchResult result) { void testSetLastRootLevelIfMultipleSet(LaunchResult result) {
CLIResult cliResult = (CLIResult) 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")); assertFalse(cliResult.getOutput().contains("INFO [org.keycloak"));
cliResult.assertStartedDevMode(); cliResult.assertStartedDevMode();
} }
@ -85,7 +85,7 @@ public class LoggingDistTest {
@Launch({ "start-dev", "--log-level=\"off,org.keycloak:warn,debug\"" }) @Launch({ "start-dev", "--log-level=\"off,org.keycloak:warn,debug\"" })
void testWinSetLastRootLevelIfMultipleSet(LaunchResult result) { void testWinSetLastRootLevelIfMultipleSet(LaunchResult result) {
CLIResult cliResult = (CLIResult) 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")); assertFalse(cliResult.getOutput().contains("INFO [org.keycloak"));
cliResult.assertStartedDevMode(); cliResult.assertStartedDevMode();
} }

View file

@ -16,6 +16,9 @@
*/ */
package org.keycloak.models; 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.jboss.resteasy.spi.ResteasyUriBuilder;
import org.keycloak.urls.HostnameProvider; import org.keycloak.urls.HostnameProvider;
import org.keycloak.urls.UrlType; import org.keycloak.urls.UrlType;
@ -26,6 +29,7 @@ import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map;
public class KeycloakUriInfo implements UriInfo { public class KeycloakUriInfo implements UriInfo {
@ -153,9 +157,24 @@ public class KeycloakUriInfo implements UriInfo {
@Override @Override
public MultivaluedMap<String, String> getQueryParameters(boolean decode) { public MultivaluedMap<String, String> getQueryParameters(boolean decode) {
if (decode) {
return delegate.getQueryParameters(decode); return delegate.getQueryParameters(decode);
} }
MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
String rawQuery = delegate.getRequestUri().getRawQuery();
if (rawQuery == null) {
return result;
}
for (Map.Entry<String, List<String>> entry : parseQueryParameters(rawQuery, false).entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
@Override @Override
public List<String> getMatchedURIs() { public List<String> getMatchedURIs() {
return delegate.getMatchedURIs(); return delegate.getMatchedURIs();

View file

@ -25,9 +25,13 @@ public interface PlatformProvider {
String name(); String name();
void onStartup(Runnable runnable); default void onStartup(Runnable runnable) {
void onShutdown(Runnable runnable); }
default void onShutdown(Runnable runnable) {
}
void exit(Throwable cause); void exit(Throwable cause);

View file

@ -47,7 +47,7 @@ public class DefaultKeycloakContext implements KeycloakContext {
private ClientModel client; private ClientModel client;
private KeycloakSession session; protected KeycloakSession session;
private Map<UrlType, KeycloakUriInfo> uriInfo; private Map<UrlType, KeycloakUriInfo> uriInfo;
@ -178,4 +178,8 @@ public class DefaultKeycloakContext implements KeycloakContext {
protected HttpResponse createHttpResponse() { protected HttpResponse createHttpResponse() {
return new HttpResponseImpl(session, getContextObject(org.jboss.resteasy.spi.HttpResponse.class)); return new HttpResponseImpl(session, getContextObject(org.jboss.resteasy.spi.HttpResponse.class));
} }
protected KeycloakSession getSession() {
return session;
}
} }

View file

@ -1,14 +1,21 @@
package org.keycloak.services.error; 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 com.fasterxml.jackson.core.JsonProcessingException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.Failure; import org.jboss.resteasy.spi.Failure;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.OAuthErrorException;
import org.keycloak.forms.login.freemarker.model.UrlBean; import org.keycloak.forms.login.freemarker.model.UrlBean;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionTaskWithResult;
import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
@ -22,7 +29,6 @@ import org.keycloak.utils.MediaType;
import org.keycloak.utils.MediaTypeMatcher; import org.keycloak.utils.MediaTypeMatcher;
import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.ExceptionMapper;
@ -43,11 +49,21 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
public static final String UNCAUGHT_SERVER_ERROR_TEXT = "Uncaught server error"; public static final String UNCAUGHT_SERVER_ERROR_TEXT = "Uncaught server error";
public static final String ERROR_RESPONSE_TEXT = "Error response {0}"; public static final String ERROR_RESPONSE_TEXT = "Error response {0}";
@Context
KeycloakSession session;
@Override @Override
public Response toResponse(Throwable throwable) { 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<Response>() {
@Override
public Response run(KeycloakSession session) {
return getResponse(session, throwable);
}
});
}
return getResponse(session, throwable); return getResponse(session, throwable);
} }
@ -118,6 +134,12 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
} }
private static String getErrorCode(Throwable throwable) { 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) { if (throwable instanceof WebApplicationException && throwable.getMessage() != null) {
return throwable.getMessage(); return throwable.getMessage();
} }

View file

@ -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<MismatchedInputException> {
@Context
KeycloakSession session;
/**
* Return escaped original message
*/
@Override
public Response toResponse(MismatchedInputException exception) {
return KeycloakErrorHandler.getResponse(session, exception);
}
}

View file

@ -45,6 +45,7 @@ import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.error.KeycloakErrorHandler; import org.keycloak.services.error.KeycloakErrorHandler;
import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler; import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler;
import org.keycloak.services.error.KeycloakMismatchedInputExceptionHandler;
import org.keycloak.services.filters.KeycloakSecurityHeadersFilter; import org.keycloak.services.filters.KeycloakSecurityHeadersFilter;
import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
@ -90,14 +91,13 @@ public class KeycloakApplication extends Application {
logger.debugv("PlatformProvider: {0}", platform.getClass().getName()); logger.debugv("PlatformProvider: {0}", platform.getClass().getName());
logger.debugv("RestEasy provider: {0}", Resteasy.getProvider().getClass().getName()); logger.debugv("RestEasy provider: {0}", Resteasy.getProvider().getClass().getName());
CryptoIntegration.init(KeycloakApplication.class.getClassLoader());
loadConfig(); loadConfig();
singletons.add(new RobotsResource()); classes.add(RobotsResource.class);
singletons.add(new RealmsResource()); classes.add(RealmsResource.class);
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_API)) { if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_API)) {
singletons.add(new AdminRoot()); classes.add(AdminRoot.class);
} }
classes.add(ThemeResource.class); classes.add(ThemeResource.class);
@ -108,9 +108,10 @@ public class KeycloakApplication extends Application {
classes.add(KeycloakSecurityHeadersFilter.class); classes.add(KeycloakSecurityHeadersFilter.class);
classes.add(KeycloakErrorHandler.class); classes.add(KeycloakErrorHandler.class);
classes.add(KcUnrecognizedPropertyExceptionHandler.class); classes.add(KcUnrecognizedPropertyExceptionHandler.class);
classes.add(KeycloakMismatchedInputExceptionHandler.class);
singletons.add(new ObjectMapperResolver()); singletons.add(new ObjectMapperResolver());
singletons.add(new WelcomeResource()); classes.add(WelcomeResource.class);
platform.onStartup(this::startup); platform.onStartup(this::startup);
platform.onShutdown(this::shutdown); platform.onShutdown(this::shutdown);
@ -122,6 +123,7 @@ public class KeycloakApplication extends Application {
} }
protected void startup() { protected void startup() {
CryptoIntegration.init(KeycloakApplication.class.getClassLoader());
KeycloakApplication.sessionFactory = createSessionFactory(); KeycloakApplication.sessionFactory = createSessionFactory();
ExportImportManager[] exportImportManager = new ExportImportManager[1]; ExportImportManager[] exportImportManager = new ExportImportManager[1];

View file

@ -137,7 +137,7 @@ public class OAuth2Error {
bearer.setErrorDescription(errorDescription); bearer.setErrorDescription(errorDescription);
WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer); WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer);
wwwAuthenticate.build(builder::header); wwwAuthenticate.build(builder::header);
builder.entity(""); builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE);
} }
return constructor.newInstance(builder.build()); return constructor.newInstance(builder.build());

View file

@ -12,6 +12,7 @@ import org.hamcrest.CoreMatchers;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
@ -93,7 +94,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(400, response.getStatusLine().getStatusCode());
OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class); OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class);
assertEquals("unknown_error", error.getError()); assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError());
assertNull(error.getErrorDescription()); assertNull(error.getErrorDescription());
} }
} }
@ -113,7 +114,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(400, response.getStatusLine().getStatusCode());
OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class); OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class);
assertEquals("unknown_error", error.getError()); assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError());
assertNull(error.getErrorDescription()); assertNull(error.getErrorDescription());
} }
} }

View file

@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.junit.Assert; import org.junit.Assert;
@ -33,7 +34,10 @@ import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.federation.kerberos.KerberosFederationProvider; import org.keycloak.federation.kerberos.KerberosFederationProvider;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel; 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.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.LDAPRule; import org.keycloak.testsuite.util.LDAPRule;
@ -205,4 +209,40 @@ public class LDAPAdminRestApiTest extends AbstractLDAPTest {
// Expected // 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<ComponentRepresentation> storageProviders = testRealm().components().query(testRealm().toRepresentation().getId(), UserStorageProvider.class.getName());
ComponentRepresentation ldapProvider = storageProviders.get(0);
List<String> 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<UserRepresentation> 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());
}
}
} }