further refinement of context handling (#28182)

* fully removing providers and moving the keycloaksession creation / final
cleanup

also deprecated Resteasy utility methods

closes: #29223

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Steven Hawkins 2024-05-02 11:21:01 -04:00 committed by GitHub
parent 3b1ca46be2
commit 4697cc956b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 163 additions and 289 deletions

View file

@ -18,26 +18,18 @@
package org.keycloak.common.util; package org.keycloak.common.util;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader;
/** /**
* <p>Provides a layer of indirection to abstract invocations to Resteasy internal APIs for obtaining the KeycloakSession * <p>Provides a way for obtaining the KeycloakSession
* *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*
* @deprecated use org.keycloak.util.KeycloakSessionUtil instead
*/ */
@Deprecated
public final class Resteasy { public final class Resteasy {
private static ResteasyProvider provider;
static {
Iterator<ResteasyProvider> iter = ServiceLoader.load(ResteasyProvider.class, Resteasy.class.getClassLoader()).iterator();
if (iter.hasNext()) {
provider = iter.next();
}
}
private static final ThreadLocal<Map<Class<?>, Object>> contextualData = new ThreadLocal<Map<Class<?>, Object>>() { private static final ThreadLocal<Map<Class<?>, Object>> contextualData = new ThreadLocal<Map<Class<?>, Object>>() {
@Override @Override
protected Map<Class<?>, Object> initialValue() { protected Map<Class<?>, Object> initialValue() {
@ -45,12 +37,8 @@ public final class Resteasy {
}; };
}; };
public static ResteasyProvider getProvider() {
return provider;
}
/** /**
* Push the given {@code instance} with type/key {@code type} to the Resteasy context associated with the current thread. * Push the given {@code instance} with type/key {@code type} to the context associated with the current thread.
* <br>Should not be called directly * <br>Should not be called directly
* *
* @param type the type/key to associate the {@code instance} with * @param type the type/key to associate the {@code instance} with
@ -61,7 +49,7 @@ public final class Resteasy {
} }
/** /**
* Clear the Resteasy context associated with the current thread. * Clear the context associated with the current thread.
* <br>Should not be called directly * <br>Should not be called directly
*/ */
public static void clearContextData() { public static void clearContextData() {
@ -69,18 +57,14 @@ public final class Resteasy {
} }
/** /**
* Lookup the instance associated with the given type/key {@code type} from the Resteasy context associated with the current thread, or from the provider. * Lookup the instance associated with the given type/key {@code type} from the context associated with the current thread.
* <br> Should only be used to obtain the KeycloakSession * <br> Should only be used to obtain the KeycloakSession
* *
* @param type the type/key to lookup * @param type the type/key to lookup
* @return the instance associated with the given {@code type} or null if non-existent. * @return the instance associated with the given {@code type} or null if non-existent.
*/ */
public static <R> R getContextData(Class<R> type) { public static <R> R getContextData(Class<R> type) {
R result = (R) contextualData.get().get(type); return (R) contextualData.get().get(type);
if (result != null) {
return result;
}
return provider.getContextData(type);
} }
/** /**
@ -88,7 +72,6 @@ public final class Resteasy {
* *
* @param type the type/key to associate the {@code instance} with * @param type the type/key to associate the {@code instance} with
* @param instance the instance * @param instance the instance
* @deprecated use {@link #pushContext(Class, Object)}
*/ */
@Deprecated @Deprecated
public static void pushDefaultContextObject(Class type, Object instance) { public static void pushDefaultContextObject(Class type, Object instance) {

View file

@ -1,7 +0,0 @@
package org.keycloak.common.util;
public interface ResteasyProvider {
<R> R getContextData(Class<R> type);
}

View file

@ -301,3 +301,9 @@ the `SingleUseObjectKeyModel` also changed to keep consistency with the method n
The previous `getExpiration` method is now deprecated and you should prefer using new newly introduced `getExp` method The previous `getExpiration` method is now deprecated and you should prefer using new newly introduced `getExp` method
to avoid overflow after 2038. to avoid overflow after 2038.
= Resteasy util class is deprecated
`org.keycloak.common.util.Resteasy` has been deprecated. You should use the `org.keycloak.util.KeycloakSessionUtil` to obtain the `KeycloakSession` instead.
It is highly recommended to avoid obtaining the `KeycloakSession` by means other than when creating your custom provider.

View file

@ -40,7 +40,6 @@ import org.keycloak.Config;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
import org.keycloak.common.enums.SslRequired; import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.Resteasy;
import org.keycloak.config.HostnameV1Options; import org.keycloak.config.HostnameV1Options;
import org.keycloak.config.ProxyOptions; import org.keycloak.config.ProxyOptions;
import org.keycloak.config.ProxyOptions.Mode; import org.keycloak.config.ProxyOptions.Mode;
@ -50,6 +49,7 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.urls.HostnameProvider; import org.keycloak.urls.HostnameProvider;
import org.keycloak.urls.HostnameProviderFactory; import org.keycloak.urls.HostnameProviderFactory;
import org.keycloak.urls.UrlType; import org.keycloak.urls.UrlType;
import org.keycloak.utils.KeycloakSessionUtil;
public final class DefaultHostnameProvider implements HostnameProvider, HostnameProviderFactory, EnvironmentDependentProviderFactory { public final class DefaultHostnameProvider implements HostnameProvider, HostnameProviderFactory, EnvironmentDependentProviderFactory {
@ -195,7 +195,7 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
} }
protected URI getRealmFrontEndUrl() { protected URI getRealmFrontEndUrl() {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
if (session == null) { if (session == null) {
return null; return null;

View file

@ -17,21 +17,27 @@
package org.keycloak.quarkus.runtime.integration.cdi; package org.keycloak.quarkus.runtime.integration.cdi;
import io.quarkus.arc.Unremovable;
import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler;
import org.keycloak.utils.KeycloakSessionUtil;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.Disposes;
import org.keycloak.common.util.Resteasy;
import org.keycloak.models.KeycloakSession;
import io.quarkus.arc.Unremovable;
@ApplicationScoped @ApplicationScoped
@Unremovable @Unremovable
public class KeycloakBeanProducer { public class KeycloakBeanProducer implements TransactionalSessionHandler {
@Produces
@RequestScoped @RequestScoped
public KeycloakSession getKeycloakSession() { public KeycloakSession getKeycloakSession() {
return Resteasy.getContextData(KeycloakSession.class); return create();
}
void dispose(@Disposes KeycloakSession session) {
KeycloakSessionUtil.setKeycloakSession(null);
close(session);
} }
} }

View file

@ -28,14 +28,12 @@ import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.core.StreamingOutput; import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.Provider;
import org.keycloak.common.util.Resteasy; import org.keycloak.utils.KeycloakSessionUtil;
import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler;
@Provider @Provider
@PreMatching @PreMatching
@Priority(1) @Priority(1)
public class CloseSessionHandler implements ContainerResponseFilter, TransactionalSessionHandler { public class CloseSessionFilter implements ContainerResponseFilter, org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler {
@Override @Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
@ -66,6 +64,6 @@ public class CloseSessionHandler implements ContainerResponseFilter, Transaction
} }
private void closeSession() { private void closeSession() {
close(Resteasy.getContextData(KeycloakSession.class)); close(KeycloakSessionUtil.getKeycloakSession());
} }
} }

View file

@ -17,108 +17,13 @@
package org.keycloak.quarkus.runtime.integration.jaxrs; 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.Map;
import java.util.Set;
import jakarta.ws.rs.core.MultivaluedMap;
public final class EmptyMultivaluedMap<K, V> implements MultivaluedMap<K, V> { import jakarta.ws.rs.core.AbstractMultivaluedMap;
@Override public final class EmptyMultivaluedMap<K, V> extends AbstractMultivaluedMap<K, V> {
public void putSingle(K key, V value) {
throw new UnsupportedOperationException();
}
@Override public EmptyMultivaluedMap() {
public void add(K key, V value) { super(Map.of());
throw new UnsupportedOperationException();
}
@Override
public V getFirst(K key) {
return null;
}
@Override
public void addAll(K key, V... newValues) {
throw new UnsupportedOperationException();
}
@Override
public void addAll(K key, List<V> valueList) {
throw new UnsupportedOperationException();
}
@Override
public void addFirst(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public boolean equalsIgnoreValueOrder(MultivaluedMap<K, V> otherMap) {
return false;
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public boolean containsKey(Object key) {
return false;
}
@Override
public boolean containsValue(Object value) {
return false;
}
@Override
public List<V> get(Object key) {
return null;
}
@Override
public List<V> put(K key, List<V> value) {
throw new UnsupportedOperationException();
}
@Override
public List<V> remove(Object key) {
throw new UnsupportedOperationException();
}
@Override
public void putAll(Map<? extends K, ? extends List<V>> m) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public Set<K> keySet() {
return emptySet();
}
@Override
public Collection<List<V>> values() {
return emptySet();
}
@Override
public Set<Entry<K, List<V>>> entrySet() {
return emptySet();
} }
} }

View file

@ -1,56 +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.resteasy;
import io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext;
import io.vertx.ext.web.RoutingContext;
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 static org.keycloak.common.util.Resteasy.clearContextData;
public final class CreateSessionHandler implements ServerRestHandler, TransactionalSessionHandler {
@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
KeycloakSession session = create();
routingContext.put(KeycloakSession.class.getName(), session);
// the CloseSessionFilter is needed because it runs sooner than this callback
// this is just a catch-all if the CloseSessionFilter doesn't get a chance to run
context.registerCompletionCallback(ignored -> {
try {
close(session);
} catch (Exception e) {
}
clearContextData();
});
}
}
}

View file

@ -32,7 +32,7 @@ import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
public final class KeycloakHandlerChainCustomizer implements HandlerChainCustomizer { public final class KeycloakHandlerChainCustomizer implements HandlerChainCustomizer {
private final CreateSessionHandler TRANSACTIONAL_SESSION_HANDLER = new CreateSessionHandler(); private final TransactionalSessionHandler TRANSACTIONAL_SESSION_HANDLER = new TransactionalSessionHandler();
private final FormBodyHandler formBodyHandler = new FormBodyHandler(true, () -> Runnable::run, Set.of()); private final FormBodyHandler formBodyHandler = new FormBodyHandler(true, () -> Runnable::run, Set.of());

View file

@ -1,45 +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.resteasy;
import io.quarkus.arc.Arc;
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
import io.vertx.ext.web.RoutingContext;
import org.keycloak.common.util.ResteasyProvider;
import java.util.Optional;
import jakarta.enterprise.context.ContextNotActiveException;
public class ResteasyVertxProvider implements ResteasyProvider {
@Override
public <R> R getContextData(Class<R> type) {
return (R) getRoutingContext().map(c -> c.get(type.getName())).orElse(null);
}
private static Optional<RoutingContext> getRoutingContext() {
try {
return Optional.ofNullable(Arc.container())
.map(c -> c.instance(CurrentVertxRequest.class).get()).map(CurrentVertxRequest::getCurrent);
} catch (ContextNotActiveException e) {
return Optional.empty();
}
}
}

View file

@ -0,0 +1,39 @@
/*
* 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 io.quarkus.arc.Arc;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
import org.keycloak.models.KeycloakSession;
import org.keycloak.utils.KeycloakSessionUtil;
public final class TransactionalSessionHandler implements ServerRestHandler, org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler {
@Override
public void handle(ResteasyReactiveRequestContext requestContext) {
requestContext.requireCDIRequestScope();
KeycloakSession currentSession = Arc.container().instance(KeycloakSession.class).get();
// this handler might be invoked multiple times when resolving sub-resources
// make sure the transaction is began once when the session is first associated with the thread
if (KeycloakSessionUtil.setKeycloakSession(currentSession) == null) {
beginTransaction(currentSession);
}
}
}

View file

@ -19,9 +19,7 @@ package org.keycloak.quarkus.runtime.transaction;
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.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
import org.keycloak.services.DefaultKeycloakSession;
/** /**
* <p>A {@link TransactionalSessionHandler} is responsible for managing transaction sessions and its lifecycle. Its subtypes * <p>A {@link TransactionalSessionHandler} is responsible for managing transaction sessions and its lifecycle. Its subtypes
@ -31,24 +29,31 @@ import org.keycloak.services.DefaultKeycloakSession;
public interface TransactionalSessionHandler { public interface TransactionalSessionHandler {
/** /**
* Creates a transactional {@link KeycloakSession}. * Creates a {@link KeycloakSession}.
* *
* @return a transactional keycloak session * @return a keycloak session
*/ */
default KeycloakSession create() { default KeycloakSession create() {
KeycloakSessionFactory sessionFactory = QuarkusKeycloakSessionFactory.getInstance(); KeycloakSessionFactory sessionFactory = QuarkusKeycloakSessionFactory.getInstance();
KeycloakSession session = sessionFactory.create(); return sessionFactory.create();
session.getTransactionManager().begin();
return session;
} }
/** /**
* Closes a transactional {@link KeycloakSession}. * begin a transaction if possible
* *
* @param session a transactional session * @param session a session
*/
default void beginTransaction(KeycloakSession session) {
session.getTransactionManager().begin();
}
/**
* Closes a {@link KeycloakSession}.
*
* @param session a session
*/ */
default void close(KeycloakSession session) { default void close(KeycloakSession session) {
if (session == null || DefaultKeycloakSession.class.cast(session).isClosed()) { if (session == null || session.isClosed()) {
return; return;
} }

View file

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

View file

@ -45,6 +45,6 @@ public class JaxRsDistTest {
assertEquals(200, when().get("/").getStatusCode()); assertEquals(200, when().get("/").getStatusCode());
Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAsserted( Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAsserted(
() -> cliResult.assertMessage("Request GET / has context request true has keycloaksession false")); () -> cliResult.assertMessage("Request GET / has context request true has keycloaksession true"));
} }
} }

View file

@ -25,7 +25,6 @@ import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils; import org.keycloak.common.util.PemUtils;
import org.keycloak.common.util.Resteasy;
import org.keycloak.common.util.SecretGenerator; import org.keycloak.common.util.SecretGenerator;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
@ -53,6 +52,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.transaction.JtaTransactionManagerLookup; import org.keycloak.transaction.JtaTransactionManagerLookup;
import org.keycloak.utils.KeycloakSessionUtil;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import jakarta.transaction.InvalidTransactionException; import jakarta.transaction.InvalidTransactionException;
@ -380,7 +380,7 @@ public final class KeycloakModelUtils {
V result; V result;
try (KeycloakSession session = factory.create()) { try (KeycloakSession session = factory.create()) {
session.getTransactionManager().begin(); session.getTransactionManager().begin();
KeycloakSession old = Resteasy.pushContext(KeycloakSession.class, session); KeycloakSession old = KeycloakSessionUtil.setKeycloakSession(session);
try { try {
cloneContextRealmClientToSession(context, session); cloneContextRealmClientToSession(context, session);
result = callable.run(session); result = callable.run(session);
@ -388,7 +388,7 @@ public final class KeycloakModelUtils {
session.getTransactionManager().setRollbackOnly(); session.getTransactionManager().setRollbackOnly();
throw t; throw t;
} finally { } finally {
Resteasy.pushContext(KeycloakSession.class, old); KeycloakSessionUtil.setKeycloakSession(old);
} }
} }
return result; return result;

View file

@ -23,13 +23,13 @@ import java.util.function.BiConsumer;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.ResponseBuilder;
import org.keycloak.common.util.Resteasy;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.http.HttpResponse; import org.keycloak.http.HttpResponse;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.utils.KeycloakSessionUtil;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -54,12 +54,12 @@ public interface Cors extends Provider {
public static final String INCLUDE_REDIRECTS = "+"; public static final String INCLUDE_REDIRECTS = "+";
public static Cors add(HttpRequest request, ResponseBuilder response) { public static Cors add(HttpRequest request, ResponseBuilder response) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
return session.getProvider(Cors.class).request(request).builder(response); return session.getProvider(Cors.class).request(request).builder(response);
} }
public static Cors add(HttpRequest request) { public static Cors add(HttpRequest request) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
return session.getProvider(Cors.class).request(request); return session.getProvider(Cors.class).request(request);
} }

View file

@ -94,6 +94,7 @@ public interface KeycloakSession extends AutoCloseable {
* @return * @return
* @deprecated Deprecated in favor of {@link #getComponentProvider) * @deprecated Deprecated in favor of {@link #getComponentProvider)
*/ */
@Deprecated
<T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel); <T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel);
/** /**
@ -203,6 +204,7 @@ public interface KeycloakSession extends AutoCloseable {
SingleUseObjectProvider singleUseObjects(); SingleUseObjectProvider singleUseObjects();
@Override
void close(); void close();
/** /**
@ -243,4 +245,6 @@ public interface KeycloakSession extends AutoCloseable {
*/ */
ClientPolicyManager clientPolicy(); ClientPolicyManager clientPolicy();
boolean isClosed();
} }

View file

@ -0,0 +1,49 @@
/*
* Copyright 2024 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.utils;
import org.keycloak.common.util.Resteasy;
import org.keycloak.models.KeycloakSession;
public class KeycloakSessionUtil {
private KeycloakSessionUtil() {
}
/**
* Get the {@link KeycloakSession} currently associated with the thread.
*
* @return the current session
*/
public static KeycloakSession getKeycloakSession() {
return Resteasy.getContextData(KeycloakSession.class);
}
/**
* Associate the {@link KeycloakSession} with the current thread.
* <br>Warning: should not be called directly. Keycloak will manage this.
*
* @param session
* @return the existing {@link KeycloakSession} or null
*/
public static KeycloakSession setKeycloakSession(KeycloakSession session) {
return Resteasy.pushContext(KeycloakSession.class, session);
}
}

View file

@ -119,13 +119,9 @@ public abstract class DefaultKeycloakContext implements KeycloakContext {
@Override @Override
public ClientConnection getConnection() { public ClientConnection getConnection() {
if (clientConnection == null) {
synchronized (this) {
if (clientConnection == null) { if (clientConnection == null) {
clientConnection = createClientConnection(); clientConnection = createClientConnection();
} }
}
}
return clientConnection; return clientConnection;
} }
@ -147,26 +143,18 @@ public abstract class DefaultKeycloakContext implements KeycloakContext {
@Override @Override
public HttpRequest getHttpRequest() { public HttpRequest getHttpRequest() {
if (request == null) {
synchronized (this) {
if (request == null) { if (request == null) {
request = createHttpRequest(); request = createHttpRequest();
} }
}
}
return request; return request;
} }
@Override @Override
public HttpResponse getHttpResponse() { public HttpResponse getHttpResponse() {
if (response == null) {
synchronized (this) {
if (response == null) { if (response == null) {
response = createHttpResponse(); response = createHttpResponse();
} }
}
}
return response; return response;
} }

View file

@ -17,9 +17,9 @@
package org.keycloak.services; package org.keycloak.services;
import org.keycloak.common.util.Resteasy;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.utils.KeycloakSessionUtil;
import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
@ -55,7 +55,7 @@ public class ErrorResponseException extends WebApplicationException {
@Override @Override
public Response getResponse() { public Response getResponse() {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
if (session != null) { if (session != null) {
// This has to happen, since calling getResponse() with non-null result leads to // This has to happen, since calling getResponse() with non-null result leads to
// directly returning the result instead of // directly returning the result instead of

View file

@ -1,6 +1,5 @@
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 static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParseException;
@ -26,6 +25,7 @@ import org.keycloak.theme.beans.MessageBean;
import org.keycloak.theme.beans.MessageFormatterMethod; import org.keycloak.theme.beans.MessageFormatterMethod;
import org.keycloak.forms.login.MessageType; import org.keycloak.forms.login.MessageType;
import org.keycloak.theme.freemarker.FreeMarkerProvider; import org.keycloak.theme.freemarker.FreeMarkerProvider;
import org.keycloak.utils.KeycloakSessionUtil;
import org.keycloak.utils.MediaType; import org.keycloak.utils.MediaType;
import org.keycloak.utils.MediaTypeMatcher; import org.keycloak.utils.MediaTypeMatcher;
@ -55,7 +55,7 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
@Override @Override
public Response toResponse(Throwable throwable) { public Response toResponse(Throwable throwable) {
KeycloakSession session = getContextData(KeycloakSession.class); KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
if (session == null) { if (session == null) {
// errors might be thrown when handling errors from JAX-RS before the session is available // errors might be thrown when handling errors from JAX-RS before the session is available

View file

@ -16,9 +16,9 @@
*/ */
package org.keycloak.services.filters; package org.keycloak.services.filters;
import org.keycloak.common.util.Resteasy;
import org.keycloak.headers.SecurityHeadersProvider; import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.utils.KeycloakSessionUtil;
import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestContext;
@ -38,7 +38,7 @@ public class KeycloakSecurityHeadersFilter implements ContainerResponseFilter {
@Override @Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) { public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class); KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
if (session != null) { if (session != null) {
SecurityHeadersProvider securityHeadersProvider = session.getProvider(SecurityHeadersProvider.class); SecurityHeadersProvider securityHeadersProvider = session.getProvider(SecurityHeadersProvider.class);