parent
2ccb6871e4
commit
217a09ce46
47 changed files with 1083 additions and 563 deletions
|
@ -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>();
|
||||
if (queryString == null || queryString.equals("")) return map;
|
||||
|
||||
|
@ -71,9 +71,9 @@ public class UriUtils {
|
|||
String[] nv = param.split("=", 2);
|
||||
try
|
||||
{
|
||||
String name = URLDecoder.decode(nv[0], "UTF-8");
|
||||
String name = decode ? URLDecoder.decode(nv[0], "UTF-8") : nv[0];
|
||||
String val = nv.length > 1 ? nv[1] : "";
|
||||
map.add(name, URLDecoder.decode(val, "UTF-8"));
|
||||
map.add(name, decode ? URLDecoder.decode(val, "UTF-8") : val);
|
||||
}
|
||||
catch (UnsupportedEncodingException e)
|
||||
{
|
||||
|
@ -84,7 +84,7 @@ public class UriUtils {
|
|||
{
|
||||
try
|
||||
{
|
||||
String name = URLDecoder.decode(param, "UTF-8");
|
||||
String name = decode ? URLDecoder.decode(param, "UTF-8") : param;
|
||||
map.add(name, "");
|
||||
}
|
||||
catch (UnsupportedEncodingException e)
|
||||
|
@ -96,6 +96,10 @@ public class UriUtils {
|
|||
return map;
|
||||
}
|
||||
|
||||
public static MultivaluedHashMap<String, String> decodeQueryString(String queryString) {
|
||||
return parseQueryParameters(queryString, true);
|
||||
}
|
||||
|
||||
public static String stripQueryParam(String url, String name){
|
||||
return url.replaceFirst("[\\?&]"+name+"=[^&]*$|"+name+"=[^&]*&", "");
|
||||
}
|
||||
|
|
0
federation/ldap/src/main/resources/META-INF/beans.xml
Normal file
0
federation/ldap/src/main/resources/META-INF/beans.xml
Normal file
|
@ -49,11 +49,11 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-deployment</artifactId>
|
||||
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-jackson-deployment</artifactId>
|
||||
<artifactId>quarkus-resteasy-reactive-jackson-deployment</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
|
|
|
@ -40,11 +40,9 @@ import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig;
|
|||
import io.quarkus.hibernate.orm.deployment.PersistenceXmlDescriptorBuildItem;
|
||||
import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem;
|
||||
import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem;
|
||||
import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentCustomizerBuildItem;
|
||||
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
|
||||
import io.quarkus.runtime.configuration.ConfigurationException;
|
||||
import io.quarkus.runtime.configuration.ProfileManager;
|
||||
import io.quarkus.vertx.http.deployment.FilterBuildItem;
|
||||
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
|
||||
import io.quarkus.vertx.http.deployment.RouteBuildItem;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
|
@ -55,8 +53,10 @@ import org.jboss.jandex.AnnotationTarget;
|
|||
import org.jboss.jandex.ClassInfo;
|
||||
import org.jboss.jandex.DotName;
|
||||
import org.jboss.jandex.IndexView;
|
||||
import org.jboss.jandex.MethodInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
|
||||
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
|
||||
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;
|
||||
import org.jboss.resteasy.spi.ResteasyDeployment;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticatorSpi;
|
||||
|
@ -93,7 +93,7 @@ import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
|
|||
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
|
||||
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer;
|
||||
import org.keycloak.quarkus.runtime.integration.web.NotFoundHandler;
|
||||
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
|
||||
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory;
|
||||
|
@ -101,6 +101,7 @@ import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFac
|
|||
import org.keycloak.representations.provider.ScriptProviderDescriptor;
|
||||
import org.keycloak.representations.provider.ScriptProviderMetadata;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.keycloak.theme.ClasspathThemeProviderFactory;
|
||||
import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
|
||||
import org.keycloak.theme.FolderThemeProviderFactory;
|
||||
|
@ -181,10 +182,6 @@ class KeycloakProcessor {
|
|||
ClasspathThemeResourceProviderFactory.class,
|
||||
JarThemeProviderFactory.class,
|
||||
JpaMapStorageProviderFactory.class);
|
||||
public static final String QUARKUS_HEALTH_ROOT_PROPERTY = "quarkus.smallrye-health.root-path";
|
||||
public static final String QUARKUS_METRICS_PATH_PROPERTY = "quarkus.micrometer.export.prometheus.path";
|
||||
public static final String QUARKUS_DEFAULT_HEALTH_PATH = "health";
|
||||
public static final String QUARKUS_DEFAULT_METRICS_PATH = "metrics";
|
||||
|
||||
static {
|
||||
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
|
||||
|
@ -593,27 +590,6 @@ class KeycloakProcessor {
|
|||
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-jpa"));
|
||||
}
|
||||
|
||||
@Record(ExecutionTime.RUNTIME_INIT)
|
||||
@BuildStep
|
||||
void initializeFilter(BuildProducer<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
|
||||
void disableMetricsEndpoint(BuildProducer<RouteBuildItem> routes) {
|
||||
if (!isMetricsEnabled()) {
|
||||
|
@ -641,15 +617,19 @@ class KeycloakProcessor {
|
|||
}
|
||||
|
||||
@BuildStep
|
||||
void configureResteasy(BuildProducer<ResteasyDeploymentCustomizerBuildItem> deploymentCustomizerProducer) {
|
||||
deploymentCustomizerProducer.produce(new ResteasyDeploymentCustomizerBuildItem(new Consumer<ResteasyDeployment>() {
|
||||
void configureResteasy(CombinedIndexBuildItem index,
|
||||
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
|
||||
public void accept(ResteasyDeployment resteasyDeployment) {
|
||||
// we need to explicitly set the application to avoid errors at build time due to the application
|
||||
// from keycloak-services also being added to the index
|
||||
resteasyDeployment.setApplicationClass(QuarkusKeycloakApplication.class.getName());
|
||||
// we need to disable the sanitizer to avoid escaping text/html responses from the server
|
||||
resteasyDeployment.setProperty(ResteasyContextParameters.RESTEASY_DISABLE_HTML_SANITIZER, Boolean.TRUE);
|
||||
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
|
||||
Map<String, Object> methodContext) {
|
||||
return List.of(chainCustomizer);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -41,11 +41,11 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy</artifactId>
|
||||
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-jackson</artifactId>
|
||||
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
|
|
|
@ -42,7 +42,6 @@ import org.keycloak.common.crypto.FipsMode;
|
|||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
|
||||
import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter;
|
||||
import org.keycloak.quarkus.runtime.storage.database.liquibase.FastServiceLocator;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
@ -141,29 +140,6 @@ public class KeycloakRecorder {
|
|||
};
|
||||
}
|
||||
|
||||
public QuarkusRequestFilter createRequestFilter(List<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) {
|
||||
String cryptoProvider = fipsMode.getProviderClassName();
|
||||
|
||||
|
|
|
@ -107,6 +107,16 @@ public final class Configuration {
|
|||
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) {
|
||||
return getOptionalValue(name).map(Boolean::parseBoolean);
|
||||
}
|
||||
|
|
|
@ -174,6 +174,11 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
|||
|
||||
protected URI getRealmFrontEndUrl() {
|
||||
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
|
||||
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
if (realm == null) {
|
||||
|
@ -300,8 +305,7 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
|||
}
|
||||
|
||||
private int getRequestPort(UriInfo uriInfo) {
|
||||
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
|
||||
return session.getContext().getHttpRequest().getUri().getBaseUri().getPort();
|
||||
return uriInfo.getBaseUri().getPort();
|
||||
}
|
||||
|
||||
private <T> T fromBaseUriOrDefault(Function<URI, T> resolver, URI baseUri, T defaultValue) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -18,11 +18,12 @@
|
|||
package org.keycloak.quarkus.runtime.integration;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.quarkus.runtime.integration.resteasy.QuarkusKeycloakContext;
|
||||
import org.keycloak.services.DefaultKeycloakContext;
|
||||
import org.keycloak.services.DefaultKeycloakSession;
|
||||
import org.keycloak.services.DefaultKeycloakSessionFactory;
|
||||
|
||||
public class QuarkusKeycloakSession extends DefaultKeycloakSession {
|
||||
public final class QuarkusKeycloakSession extends DefaultKeycloakSession {
|
||||
|
||||
public QuarkusKeycloakSession(DefaultKeycloakSessionFactory factory) {
|
||||
super(factory);
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
|
@ -77,23 +77,10 @@ public class QuarkusPlatform implements PlatformProvider {
|
|||
}
|
||||
}
|
||||
|
||||
Runnable startupHook;
|
||||
Runnable shutdownHook;
|
||||
|
||||
private AtomicBoolean started = new AtomicBoolean(false);
|
||||
private List<Throwable> deferredExceptions = new CopyOnWriteArrayList<>();
|
||||
private File tmpDir;
|
||||
|
||||
@Override
|
||||
public void onStartup(Runnable startupHook) {
|
||||
this.startupHook = startupHook;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShutdown(Runnable shutdownHook) {
|
||||
this.shutdownHook = shutdownHook;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit(Throwable cause) {
|
||||
Quarkus.asyncExit(1);
|
||||
|
@ -102,7 +89,7 @@ public class QuarkusPlatform implements PlatformProvider {
|
|||
/**
|
||||
* Called when Quarkus platform is started
|
||||
*/
|
||||
void started() {
|
||||
public void started() {
|
||||
this.started.set(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,21 +15,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.quarkus.runtime.integration;
|
||||
package org.keycloak.quarkus.runtime.integration.cdi;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import org.keycloak.common.util.Resteasy;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.DefaultKeycloakContext;
|
||||
|
||||
public class QuarkusKeycloakContext extends DefaultKeycloakContext {
|
||||
import io.quarkus.arc.Unremovable;
|
||||
|
||||
public QuarkusKeycloakContext(KeycloakSession session) {
|
||||
super(session);
|
||||
}
|
||||
@ApplicationScoped
|
||||
@Unremovable
|
||||
public class KeycloakBeanProducer {
|
||||
|
||||
@Override
|
||||
protected HttpRequest createHttpRequest() {
|
||||
return new QuarkusHttpRequest(Resteasy.getContextData(org.jboss.resteasy.spi.HttpRequest.class));
|
||||
@Produces
|
||||
@RequestScoped
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return Resteasy.getContextData(KeycloakSession.class);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -18,7 +18,10 @@
|
|||
package org.keycloak.quarkus.runtime.integration.jaxrs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.stream.Stream;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseFilter;
|
||||
|
@ -29,28 +32,40 @@ import org.keycloak.common.util.Resteasy;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
|
||||
@Provider
|
||||
@PreMatching
|
||||
@Priority(1)
|
||||
public class TransactionalResponseFilter implements ContainerResponseFilter, TransactionalSessionHandler {
|
||||
public class CloseSessionHandler implements ContainerResponseFilter, TransactionalSessionHandler {
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
|
||||
throws IOException {
|
||||
Object entity = responseContext.getEntity();
|
||||
|
||||
if (shouldDelaySessionClose(entity)) {
|
||||
if (entity instanceof Stream) {
|
||||
Stream entityStream = (Stream) entity;
|
||||
entityStream.onClose(this::closeSession);
|
||||
return;
|
||||
}
|
||||
|
||||
close(Resteasy.getContextData(KeycloakSession.class));
|
||||
if (entity instanceof StreamingOutput) {
|
||||
responseContext.setEntity(new StreamingOutput() {
|
||||
@Override
|
||||
public void write(OutputStream output) throws IOException, WebApplicationException {
|
||||
try {
|
||||
((StreamingOutput) entity).write(output);
|
||||
} finally {
|
||||
closeSession();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
closeSession();
|
||||
}
|
||||
|
||||
private static boolean shouldDelaySessionClose(Object entity) {
|
||||
// do not close the session if the response entity is a stream
|
||||
// that is because we need the session open until the stream is transformed as it might require access to the database
|
||||
return entity instanceof Stream || entity instanceof StreamingOutput;
|
||||
private void closeSession() {
|
||||
close(Resteasy.getContextData(KeycloakSession.class));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ import java.lang.reflect.Type;
|
|||
import org.jboss.resteasy.spi.ContextInjector;
|
||||
import org.keycloak.common.util.Resteasy;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter;
|
||||
import org.keycloak.quarkus.runtime.integration.resteasy.ResteasyVertxProvider;
|
||||
|
||||
/**
|
||||
* <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
|
||||
* 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 KeycloakContextInjector implements ContextInjector<KeycloakSession, KeycloakSession> {
|
||||
public class KeycloakSessionContextInjector implements ContextInjector<KeycloakSession, KeycloakSession> {
|
||||
@Override
|
||||
public KeycloakSession resolve(Class rawType, Type genericType, Annotation[] annotations) {
|
||||
return Resteasy.getContextData(KeycloakSession.class);
|
|
@ -17,26 +17,40 @@
|
|||
|
||||
package org.keycloak.quarkus.runtime.integration.jaxrs;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.ws.rs.ApplicationPath;
|
||||
|
||||
import org.keycloak.config.HostnameOptions;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.platform.Platform;
|
||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
|
||||
import org.keycloak.quarkus.runtime.integration.QuarkusPlatform;
|
||||
import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.WelcomeResource;
|
||||
|
||||
import io.quarkus.runtime.ShutdownEvent;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import io.smallrye.common.annotation.Blocking;
|
||||
|
||||
@ApplicationPath("/")
|
||||
@Blocking
|
||||
public class QuarkusKeycloakApplication extends KeycloakApplication {
|
||||
|
||||
private static boolean filterSingletons(Object o) {
|
||||
return !WelcomeResource.class.isInstance(o);
|
||||
void onStartupEvent(@Observes StartupEvent event) {
|
||||
QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform();
|
||||
platform.started();
|
||||
QuarkusPlatform.exitOnError();
|
||||
startup();
|
||||
}
|
||||
|
||||
void onShutdownEvent(@Observes ShutdownEvent event) {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,16 +67,21 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
|
|||
|
||||
@Override
|
||||
public Set<Object> getSingletons() {
|
||||
Set<Object> singletons = super.getSingletons().stream()
|
||||
.filter(QuarkusKeycloakApplication::filterSingletons)
|
||||
.collect(Collectors.toSet());
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
singletons.add(new QuarkusWelcomeResource());
|
||||
@Override
|
||||
public Set<Class<?>> getClasses() {
|
||||
Set<Class<?>> classes = new HashSet<>(super.getClasses());
|
||||
|
||||
if (Configuration.getOptionalBooleanValue("--" + HostnameOptions.HOSTNAME_DEBUG.getKey()).orElse(Boolean.FALSE)) {
|
||||
singletons.add(new DebugHostnameSettingsResource());
|
||||
}
|
||||
classes.remove(WelcomeResource.class);
|
||||
classes.add(QuarkusWelcomeResource.class);
|
||||
|
||||
return singletons;
|
||||
classes.add(QuarkusObjectMapperResolver.class);
|
||||
classes.add(CloseSessionHandler.class);
|
||||
|
||||
classes.add(DebugHostnameSettingsResource.class);
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -15,15 +15,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.quarkus.runtime.integration.jaxrs;
|
||||
package org.keycloak.quarkus.runtime.integration.resteasy;
|
||||
|
||||
import io.quarkus.arc.Arc;
|
||||
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import org.jboss.resteasy.core.ResteasyContext;
|
||||
import org.keycloak.common.util.ResteasyProvider;
|
||||
|
||||
/**
|
||||
* TODO: we should probably rely on the vert.x routing context instead of resteasy context data
|
||||
*/
|
||||
public class ResteasyVertxProvider implements ResteasyProvider {
|
||||
|
||||
@Override
|
||||
|
@ -31,7 +30,7 @@ public class ResteasyVertxProvider implements ResteasyProvider {
|
|||
R data = ResteasyContext.getContextData(type);
|
||||
|
||||
if (data == null) {
|
||||
RoutingContext contextData = ResteasyContext.getContextData(RoutingContext.class);
|
||||
RoutingContext contextData = Arc.container().instance(CurrentVertxRequest.class).get().getCurrent();
|
||||
|
||||
if (contextData == null) {
|
||||
return null;
|
||||
|
@ -44,14 +43,13 @@ public class ResteasyVertxProvider implements ResteasyProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void pushDefaultContextObject(Class type, Object instance) {
|
||||
ResteasyContext.getContextData(org.jboss.resteasy.spi.Dispatcher.class).getDefaultContextObjects()
|
||||
.put(type, instance);
|
||||
public void pushContext(Class type, Object instance) {
|
||||
ResteasyContext.pushContext(type, instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushContext(Class type, Object instance) {
|
||||
ResteasyContext.pushContext(type, instance);
|
||||
public void pushDefaultContextObject(Class type, Object instance) {
|
||||
pushContext(type, instance);
|
||||
}
|
||||
|
||||
@Override
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.quarkus.runtime.services.resources;
|
||||
|
||||
import io.quarkus.resteasy.reactive.server.EndpointDisabled;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
|
@ -43,6 +45,7 @@ import java.util.Map;
|
|||
import java.util.TreeMap;
|
||||
|
||||
@Path("/realms")
|
||||
@EndpointDisabled(name = "kc.hostname-debug", stringValue = "false", disableIfMissing = true)
|
||||
public class DebugHostnameSettingsResource {
|
||||
public static final String DEFAULT_PATH_SUFFIX = "hostname-debug";
|
||||
public static final String PATH_FOR_TEST_CORS_IN_HEADERS = "test";
|
||||
|
@ -66,9 +69,14 @@ public class DebugHostnameSettingsResource {
|
|||
@Path("/{realmName}/" + DEFAULT_PATH_SUFFIX)
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public String debug(final @PathParam("realmName") String realmName) throws IOException, FreeMarkerException {
|
||||
FreeMarkerProvider freeMarkerProvider = keycloakSession.getProvider(FreeMarkerProvider.class);
|
||||
RealmModel realmModel = keycloakSession.realms().getRealmByName(realmName);
|
||||
|
||||
if (realmModel == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
FreeMarkerProvider freeMarkerProvider = keycloakSession.getProvider(FreeMarkerProvider.class);
|
||||
|
||||
URI frontendUri = keycloakSession.getContext().getUri(UrlType.FRONTEND).getBaseUri();
|
||||
URI backendUri = keycloakSession.getContext().getUri(UrlType.BACKEND).getBaseUri();
|
||||
URI adminUri = keycloakSession.getContext().getUri(UrlType.ADMIN).getBaseUri();
|
||||
|
|
|
@ -17,11 +17,10 @@
|
|||
|
||||
package org.keycloak.quarkus.runtime.transaction;
|
||||
|
||||
import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakTransactionManager;
|
||||
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
|
||||
import org.keycloak.services.DefaultKeycloakSession;
|
||||
|
||||
/**
|
||||
|
@ -37,7 +36,7 @@ public interface TransactionalSessionHandler {
|
|||
* @return a transactional keycloak session
|
||||
*/
|
||||
default KeycloakSession create() {
|
||||
KeycloakSessionFactory sessionFactory = getSessionFactory();
|
||||
KeycloakSessionFactory sessionFactory = QuarkusKeycloakSessionFactory.getInstance();
|
||||
KeycloakSession session = sessionFactory.create();
|
||||
session.getTransactionManager().begin();
|
||||
return session;
|
||||
|
|
|
@ -1 +1 @@
|
|||
org.keycloak.quarkus.runtime.integration.jaxrs.ResteasyVertxProvider
|
||||
org.keycloak.quarkus.runtime.integration.resteasy.ResteasyVertxProvider
|
|
@ -44,6 +44,13 @@ quarkus.log.category."io.quarkus.config".level=off
|
|||
quarkus.log.category."io.quarkus.arc.processor.BeanArchives".level=off
|
||||
quarkus.log.category."io.quarkus.arc.processor.IndexClassLookupUtils".level=off
|
||||
quarkus.log.category."io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor".level=warn
|
||||
quarkus.log.category."io.quarkus.deployment.steps.ReflectiveHierarchyStep".level=error
|
||||
|
||||
# Set default directory name for the location of the transaction logs
|
||||
quarkus.transaction-manager.object-store.directory=${kc.home.dir:default}${file.separator}data${file.separator}transaction-logs
|
||||
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
|
|
@ -19,7 +19,14 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-quarkus-server</artifactId>
|
||||
</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 -->
|
||||
<!-- Can be removed as part of the https://github.com/keycloak/keycloak/issues/22455 enhancement -->
|
||||
<dependency>
|
||||
|
|
|
@ -71,6 +71,10 @@
|
|||
<artifactId>bctls-fips</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JDBC Drivers -->
|
||||
<dependency>
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.it.cli.dist;
|
|||
|
||||
import io.quarkus.test.junit.main.Launch;
|
||||
import io.restassured.RestAssured;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -111,60 +112,55 @@ public class HostnameDistTest {
|
|||
@WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" })
|
||||
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-port=8543" })
|
||||
public void testWelcomePageAdminUrl() {
|
||||
Assert.assertTrue(when().get("http://mykeycloak.org:8080").asString().contains("http://mykeycloak.org:8080/admin/"));
|
||||
Assert.assertTrue(when().get("https://mykeycloak.org:8443").asString().contains("https://mykeycloak.org:8443/admin/"));
|
||||
Assert.assertTrue(when().get("http://localhost:8080").asString().contains("http://localhost:8080/admin/"));
|
||||
Assert.assertTrue(when().get("https://localhost:8443").asString().contains("https://localhost:8443/admin/"));
|
||||
when().get("http://mykeycloak.org:8080").then().body(Matchers.containsString("http://mykeycloak.org:8080/admin/"));
|
||||
when().get("https://mykeycloak.org:8443").then().body(Matchers.containsString("https://mykeycloak.org:8443/admin/"));
|
||||
when().get("http://localhost:8080").then().body(Matchers.containsString("http://localhost:8080/admin/"));
|
||||
when().get("https://localhost:8443").then().body(Matchers.containsString("https://localhost:8443/admin/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=true" })
|
||||
public void testDebugHostnameSettingsEnabled() {
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 200);
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Configuration property"));
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("Server mode"));
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("production [start]"));
|
||||
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(200);
|
||||
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Configuration property"));
|
||||
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("Server mode"));
|
||||
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().body(Matchers.containsString("production [start]"));
|
||||
|
||||
Assert.assertTrue(
|
||||
when().get("http://mykeycloak.org:8080/realms/master/" +
|
||||
when().get("http://mykeycloak.org:8080/realms/master/" +
|
||||
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
|
||||
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
|
||||
.getStatusCode() == 200
|
||||
);
|
||||
Assert.assertTrue(
|
||||
when().get("http://localhost:8080/realms/master/" +
|
||||
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
|
||||
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
|
||||
.asString()
|
||||
.contains(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK")
|
||||
);
|
||||
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS
|
||||
).then().statusCode(200);
|
||||
when().get("http://localhost:8080/realms/master/" +
|
||||
DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX +
|
||||
"/" + DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS)
|
||||
.then()
|
||||
.body(Matchers.containsString(DebugHostnameSettingsResource.PATH_FOR_TEST_CORS_IN_HEADERS + "-OK"));
|
||||
when().get("http://localhost:8080/realms/non-existent/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-debug=false" })
|
||||
public void testDebugHostnameSettingsDisabledBySetting() {
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404);
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404"));
|
||||
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start", "--hostname=mykeycloak.org"})
|
||||
public void testDebugHostnameSettingsDisabledByDefault() {
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).getStatusCode() == 404);
|
||||
Assert.assertTrue(when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).asString().contains("404"));
|
||||
when().get("http://localhost:8080/realms/master/" + DebugHostnameSettingsResource.DEFAULT_PATH_SUFFIX).then().statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start", "--hostname=mykeycloak.org", "--hostname-admin=mykeycloakadmin.org" })
|
||||
public void testHostnameAdminSet() {
|
||||
Assert.assertTrue(when().get("https://mykeycloak.org:8443/admin/master/console").asString().contains("\"authUrl\": \"https://mykeycloakadmin.org:8443\""));
|
||||
Assert.assertTrue(when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.org:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").asString().contains("Sign in to your account"));
|
||||
when().get("https://mykeycloak.org:8443/admin/master/console").then().body(Matchers.containsString("\"authUrl\": \"https://mykeycloakadmin.org:8443\""));
|
||||
when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.org:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Sign in to your account"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start", "--hostname=mykeycloak.org" })
|
||||
public void testInvalidRedirectUriWhenAdminNotSet() {
|
||||
Assert.assertTrue(when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.127.0.0.1.nip.io:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").asString().contains("Invalid parameter: redirect_uri"));
|
||||
when().get("https://mykeycloak.org:8443/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=https://mykeycloakadmin.127.0.0.1.nip.io:8443/admin/master/console&state=02234324-d91e-4bf2-8396-57498e96b12a&response_mode=fragment&response_type=code&scope=openid&nonce=f8f3812e-e349-4bbf-8d15-cbba4927f5e5&code_challenge=7qjD_v11WGkt1ig-ZFHxJdrEvuTlzjFRgRGQ_5ADcko&code_challenge_method=S256").then().body(Matchers.containsString("Invalid parameter: redirect_uri"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -177,7 +173,7 @@ public class HostnameDistTest {
|
|||
@WithEnvVars({ "KEYCLOAK_ADMIN", "admin", "KEYCLOAK_ADMIN_PASSWORD", "admin" })
|
||||
@Launch({ "start", "--proxy=edge", "--hostname=mykeycloak.org", "--hostname-admin-url=http://mykeycloakadmin.org:1234" })
|
||||
public void testAdminUrl() {
|
||||
Assert.assertTrue(when().get("https://mykeycloak.org:8443").asString().contains("http://mykeycloakadmin.org:1234/admin/"));
|
||||
when().get("https://mykeycloak.org:8443").then().body(Matchers.containsString("http://mykeycloakadmin.org:1234/admin/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -51,7 +51,7 @@ public class LoggingDistTest {
|
|||
@Launch({ "start-dev", "--log-level=debug" })
|
||||
void testSetRootLevel(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertTrue(cliResult.getOutput().contains("DEBUG [io.quarkus.resteasy.runtime]"));
|
||||
assertTrue(cliResult.getOutput().contains("DEBUG [io.netty.util.internal"));
|
||||
cliResult.assertStartedDevMode();
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ public class LoggingDistTest {
|
|||
@Launch({ "start-dev", "--log-level=off,org.keycloak:warn,debug" })
|
||||
void testSetLastRootLevelIfMultipleSet(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertTrue(cliResult.getOutput().contains("DEBUG [io.quarkus.resteasy.runtime]"));
|
||||
assertTrue(cliResult.getOutput().contains("DEBUG [io.netty.util.internal"));
|
||||
assertFalse(cliResult.getOutput().contains("INFO [org.keycloak"));
|
||||
cliResult.assertStartedDevMode();
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ public class LoggingDistTest {
|
|||
@Launch({ "start-dev", "--log-level=\"off,org.keycloak:warn,debug\"" })
|
||||
void testWinSetLastRootLevelIfMultipleSet(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertTrue(cliResult.getOutput().contains("DEBUG [io.quarkus.resteasy.runtime]"));
|
||||
assertTrue(cliResult.getOutput().contains("DEBUG [io.netty.util.internal"));
|
||||
assertFalse(cliResult.getOutput().contains("INFO [org.keycloak"));
|
||||
cliResult.assertStartedDevMode();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
*/
|
||||
package org.keycloak.models;
|
||||
|
||||
import static org.keycloak.common.util.UriUtils.parseQueryParameters;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||
import org.jboss.resteasy.spi.ResteasyUriBuilder;
|
||||
import org.keycloak.urls.HostnameProvider;
|
||||
import org.keycloak.urls.UrlType;
|
||||
|
@ -26,6 +29,7 @@ import jakarta.ws.rs.core.UriBuilder;
|
|||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class KeycloakUriInfo implements UriInfo {
|
||||
|
||||
|
@ -153,7 +157,22 @@ public class KeycloakUriInfo implements UriInfo {
|
|||
|
||||
@Override
|
||||
public MultivaluedMap<String, String> getQueryParameters(boolean decode) {
|
||||
return delegate.getQueryParameters(decode);
|
||||
if (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
|
||||
|
|
|
@ -25,9 +25,13 @@ public interface PlatformProvider {
|
|||
|
||||
String name();
|
||||
|
||||
void onStartup(Runnable runnable);
|
||||
default void onStartup(Runnable runnable) {
|
||||
|
||||
void onShutdown(Runnable runnable);
|
||||
}
|
||||
|
||||
default void onShutdown(Runnable runnable) {
|
||||
|
||||
}
|
||||
|
||||
void exit(Throwable cause);
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ public class DefaultKeycloakContext implements KeycloakContext {
|
|||
|
||||
private ClientModel client;
|
||||
|
||||
private KeycloakSession session;
|
||||
protected KeycloakSession session;
|
||||
|
||||
private Map<UrlType, KeycloakUriInfo> uriInfo;
|
||||
|
||||
|
@ -178,4 +178,8 @@ public class DefaultKeycloakContext implements KeycloakContext {
|
|||
protected HttpResponse createHttpResponse() {
|
||||
return new HttpResponseImpl(session, getContextObject(org.jboss.resteasy.spi.HttpResponse.class));
|
||||
}
|
||||
|
||||
protected KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
package org.keycloak.services.error;
|
||||
|
||||
import static org.keycloak.common.util.Resteasy.getContextData;
|
||||
import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.Failure;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.forms.login.freemarker.model.UrlBean;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionTaskWithResult;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
@ -22,7 +29,6 @@ import org.keycloak.utils.MediaType;
|
|||
import org.keycloak.utils.MediaTypeMatcher;
|
||||
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
|
@ -43,11 +49,21 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
|
|||
public static final String UNCAUGHT_SERVER_ERROR_TEXT = "Uncaught server error";
|
||||
public static final String ERROR_RESPONSE_TEXT = "Error response {0}";
|
||||
|
||||
@Context
|
||||
KeycloakSession session;
|
||||
|
||||
@Override
|
||||
public Response toResponse(Throwable throwable) {
|
||||
KeycloakSession session = getContextData(KeycloakSession.class);
|
||||
|
||||
if (session == null) {
|
||||
// errors might be thrown when handling errors from JAX-RS before the session is available
|
||||
return KeycloakModelUtils.runJobInTransactionWithResult(getSessionFactory(),
|
||||
new KeycloakSessionTaskWithResult<Response>() {
|
||||
@Override
|
||||
public Response run(KeycloakSession session) {
|
||||
return getResponse(session, throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return getResponse(session, throwable);
|
||||
}
|
||||
|
||||
|
@ -118,6 +134,12 @@ public class KeycloakErrorHandler implements ExceptionMapper<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) {
|
||||
return throwable.getMessage();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ import org.keycloak.services.DefaultKeycloakSessionFactory;
|
|||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.error.KeycloakErrorHandler;
|
||||
import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler;
|
||||
import org.keycloak.services.error.KeycloakMismatchedInputExceptionHandler;
|
||||
import org.keycloak.services.filters.KeycloakSecurityHeadersFilter;
|
||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
|
@ -90,14 +91,13 @@ public class KeycloakApplication extends Application {
|
|||
|
||||
logger.debugv("PlatformProvider: {0}", platform.getClass().getName());
|
||||
logger.debugv("RestEasy provider: {0}", Resteasy.getProvider().getClass().getName());
|
||||
CryptoIntegration.init(KeycloakApplication.class.getClassLoader());
|
||||
|
||||
loadConfig();
|
||||
|
||||
singletons.add(new RobotsResource());
|
||||
singletons.add(new RealmsResource());
|
||||
classes.add(RobotsResource.class);
|
||||
classes.add(RealmsResource.class);
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_API)) {
|
||||
singletons.add(new AdminRoot());
|
||||
classes.add(AdminRoot.class);
|
||||
}
|
||||
classes.add(ThemeResource.class);
|
||||
|
||||
|
@ -108,9 +108,10 @@ public class KeycloakApplication extends Application {
|
|||
classes.add(KeycloakSecurityHeadersFilter.class);
|
||||
classes.add(KeycloakErrorHandler.class);
|
||||
classes.add(KcUnrecognizedPropertyExceptionHandler.class);
|
||||
classes.add(KeycloakMismatchedInputExceptionHandler.class);
|
||||
|
||||
singletons.add(new ObjectMapperResolver());
|
||||
singletons.add(new WelcomeResource());
|
||||
classes.add(WelcomeResource.class);
|
||||
|
||||
platform.onStartup(this::startup);
|
||||
platform.onShutdown(this::shutdown);
|
||||
|
@ -122,6 +123,7 @@ public class KeycloakApplication extends Application {
|
|||
}
|
||||
|
||||
protected void startup() {
|
||||
CryptoIntegration.init(KeycloakApplication.class.getClassLoader());
|
||||
KeycloakApplication.sessionFactory = createSessionFactory();
|
||||
|
||||
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
||||
|
|
|
@ -137,7 +137,7 @@ public class OAuth2Error {
|
|||
bearer.setErrorDescription(errorDescription);
|
||||
WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer);
|
||||
wwwAuthenticate.build(builder::header);
|
||||
builder.entity("");
|
||||
builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE);
|
||||
}
|
||||
|
||||
return constructor.newInstance(builder.build());
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.hamcrest.CoreMatchers;
|
|||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
|
@ -93,7 +94,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
|
|||
assertEquals(400, response.getStatusLine().getStatusCode());
|
||||
|
||||
OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class);
|
||||
assertEquals("unknown_error", error.getError());
|
||||
assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError());
|
||||
assertNull(error.getErrorDescription());
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +114,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
|
|||
assertEquals(400, response.getStatusLine().getStatusCode());
|
||||
|
||||
OAuth2ErrorRepresentation error = JsonSerialization.readValue(response.getEntity().getContent(), OAuth2ErrorRepresentation.class);
|
||||
assertEquals("unknown_error", error.getError());
|
||||
assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError());
|
||||
assertNull(error.getErrorDescription());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.junit.Assert;
|
||||
|
@ -33,7 +34,10 @@ import org.keycloak.admin.client.resource.UserResource;
|
|||
import org.keycloak.federation.kerberos.KerberosFederationProvider;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
|
@ -205,4 +209,40 @@ public class LDAPAdminRestApiTest extends AbstractLDAPTest {
|
|||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorResponseWhenLdapIsFailing() {
|
||||
// Create user just with the username
|
||||
UserRepresentation user1 = UserBuilder.create()
|
||||
.username("admintestuser1")
|
||||
.password("userpass")
|
||||
.enabled(true)
|
||||
.build();
|
||||
String newUserId1 = createUserExpectSuccess(user1);
|
||||
getCleanup().addUserId(newUserId1);
|
||||
|
||||
List<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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue