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