From 7703d813898e3b23980f69d46d61fac0b97dffde Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Thu, 8 Nov 2018 14:08:29 +0100 Subject: [PATCH] KEYCLOAK-7421 Support SAML cluster logout for Elytron SAML adapter --- adapters/saml/wildfly-elytron/pom.xml | 8 + .../saml/elytron/ElytronHttpFacade.java | 9 +- .../saml/elytron/ElytronSamlSessionStore.java | 28 ++- .../IdMapperUpdaterSessionListener.java | 106 +++++++++ .../KeycloakConfigurationServletListener.java | 93 +++++++- ...loakHttpServerAuthenticationMechanism.java | 20 +- ...pServerAuthenticationMechanismFactory.java | 7 +- ...InfinispanSessionCacheIdMapperUpdater.java | 130 +++++++++++ .../SsoCacheSessionIdMapperUpdater.java | 67 ++++++ .../infinispan/SsoSessionCacheListener.java | 212 ++++++++++++++++++ .../infinispan/SsoSessionCacheListener.java | 24 +- .../adapters/spi/InMemorySessionIdMapper.java | 8 + .../common/cli/add-adapter-log-level.cli | 1 + .../common/cli/configure-crossdc-config.cli | 14 +- .../arquillian/CrossDCTestEnricher.java | 2 +- .../AbstractSAMLAdapterClusteredTest.java | 21 +- .../crossdc/SAMLAdapterCrossDCTest.java | 3 + 17 files changed, 706 insertions(+), 47 deletions(-) create mode 100644 adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java create mode 100644 adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java create mode 100644 adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java create mode 100644 adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml index b1781f6ba1..ecc8dbf379 100755 --- a/adapters/saml/wildfly-elytron/pom.xml +++ b/adapters/saml/wildfly-elytron/pom.xml @@ -75,6 +75,14 @@ org.wildfly.security wildfly-elytron + + org.infinispan + infinispan-core + + + org.infinispan + infinispan-cachestore-remote + junit junit diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java index 8b31a31a00..a111e1dacb 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java @@ -44,6 +44,7 @@ import org.keycloak.adapters.spi.AuthenticationError; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.LogoutError; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.wildfly.security.auth.callback.AnonymousAuthorizationCallback; import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; import org.wildfly.security.auth.callback.SecurityIdentityCallback; @@ -68,16 +69,16 @@ class ElytronHttpFacade implements HttpFacade { private boolean restored; private SamlSession samlSession; - public ElytronHttpFacade(HttpServerRequest request, SessionIdMapper idMapper, SamlDeploymentContext deploymentContext, CallbackHandler handler) { + public ElytronHttpFacade(HttpServerRequest request, SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, SamlDeploymentContext deploymentContext, CallbackHandler handler) { this.request = request; this.deploymentContext = deploymentContext; this.callbackHandler = handler; this.responseConsumer = response -> {}; - this.sessionStore = createTokenStore(idMapper); + this.sessionStore = createTokenStore(idMapper, idMapperUpdater); } - private SamlSessionStore createTokenStore(SessionIdMapper idMapper) { - return new ElytronSamlSessionStore(this, idMapper, getDeployment()); + private SamlSessionStore createTokenStore(SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater) { + return new ElytronSamlSessionStore(this, idMapper, idMapperUpdater, getDeployment()); } void authenticationComplete(SamlSession samlSession) { diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java index 2ce62928df..ebe7376aa5 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java @@ -18,13 +18,10 @@ package org.keycloak.adapters.saml.elytron; import java.net.URI; -import java.security.Principal; import java.util.LinkedList; import java.util.List; import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import org.jboss.logging.Logger; import org.keycloak.adapters.saml.SamlDeployment; @@ -32,6 +29,7 @@ import org.keycloak.adapters.saml.SamlSession; import org.keycloak.adapters.saml.SamlSessionStore; import org.keycloak.adapters.saml.SamlUtil; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.common.util.KeycloakUriBuilder; import org.wildfly.security.http.HttpScope; import org.wildfly.security.http.Scope; @@ -45,13 +43,15 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI"; private final SessionIdMapper idMapper; + private final SessionIdMapperUpdater idMapperUpdater; protected final SamlDeployment deployment; private final ElytronHttpFacade exchange; - public ElytronSamlSessionStore(ElytronHttpFacade exchange, SessionIdMapper idMapper, SamlDeployment deployment) { + public ElytronSamlSessionStore(ElytronHttpFacade exchange, SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, SamlDeployment deployment) { this.exchange = exchange; this.idMapper = idMapper; + this.idMapperUpdater = idMapperUpdater; this.deployment = deployment; } @@ -81,10 +81,11 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto public void logoutAccount() { HttpScope session = getSession(false); if (session.exists()) { + log.debug("Logging out - current account"); SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName()); if (samlSession != null) { if (samlSession.getSessionIndex() != null) { - idMapper.removeSession(session.getID()); + idMapperUpdater.removeSession(idMapper, session.getID()); } session.setAttachment(SamlSession.class.getName(), null); } @@ -96,11 +97,12 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto public void logoutByPrincipal(String principal) { Set sessions = idMapper.getUserSessions(principal); if (sessions != null) { + log.debugf("Logging out - by principal: %s", sessions); List ids = new LinkedList<>(); ids.addAll(sessions); logoutSessionIds(ids); for (String id : ids) { - idMapper.removeSession(id); + idMapperUpdater.removeSession(idMapper, id); } } @@ -109,12 +111,13 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto @Override public void logoutBySsoId(List ssoIds) { if (ssoIds == null) return; + log.debugf("Logging out - by session IDs: %s", ssoIds); List sessionIds = new LinkedList<>(); for (String id : ssoIds) { String sessionId = idMapper.getSessionFromSSO(id); if (sessionId != null) { sessionIds.add(sessionId); - idMapper.removeSession(sessionId); + idMapperUpdater.removeSession(idMapper, sessionId); } } @@ -126,6 +129,8 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto HttpScope scope = exchange.getScope(Scope.SESSION, id); if (scope.exists()) { + log.debugf("Invalidating session %s", id); + scope.setAttachment(SamlSession.class.getName(), null); scope.invalidate(); } }); @@ -138,6 +143,13 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto log.debug("session was null, returning null"); return false; } + + if (! idMapper.hasSession(session.getID())) { + log.debugf("Session %s has expired on some other node", session.getID()); + session.setAttachment(SamlSession.class.getName(), null); + return false; + } + final SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName()); if (samlSession == null) { log.debug("SamlSession was not in session, returning null"); @@ -154,7 +166,7 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto HttpScope session = getSession(true); session.setAttachment(SamlSession.class.getName(), account); String sessionId = changeSessionId(session); - idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); + idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); } diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java new file mode 100644 index 0000000000..d65d74a308 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017 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.adapters.saml.elytron; + +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.spi.SessionIdMapper; + +import java.util.Objects; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener { + + private static final Logger LOG = Logger.getLogger(IdMapperUpdaterSessionListener.class); + + private final SessionIdMapper idMapper; + + public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) { + this.idMapper = idMapper; + } + + @Override + public void sessionCreated(HttpSessionEvent hse) { + LOG.debugf("Session created"); + HttpSession session = hse.getSession(); + Object value = session.getAttribute(SamlSession.class.getName()); + map(session.getId(), value); + } + + @Override + public void sessionDestroyed(HttpSessionEvent hse) { + LOG.debugf("Session destroyed"); + HttpSession session = hse.getSession(); + unmap(session.getId(), session.getAttribute(SamlSession.class.getName())); + } + + @Override + public void attributeAdded(HttpSessionBindingEvent hsbe) { + HttpSession session = hsbe.getSession(); + if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { + LOG.debugf("Attribute added"); + map(session.getId(), hsbe.getValue()); + } + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent hsbe) { + HttpSession session = hsbe.getSession(); + if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { + LOG.debugf("Attribute removed"); + unmap(session.getId(), hsbe.getValue()); + } + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent hsbe) { + HttpSession session = hsbe.getSession(); + if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { + LOG.debugf("Attribute replaced"); + unmap(session.getId(), hsbe.getValue()); + map(session.getId(), session.getAttribute(SamlSession.class.getName())); + } + } + + private void map(String sessionId, Object value) { + if (! (value instanceof SamlSession) || sessionId == null) { + return; + } + SamlSession account = (SamlSession) value; + + idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); + } + + private void unmap(String sessionId, Object value) { + if (! (value instanceof SamlSession) || sessionId == null) { + return; + } + + SamlSession samlSession = (SamlSession) value; + if (samlSession.getSessionIndex() != null) { + idMapper.removeSession(sessionId); + } + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java index 41762870aa..5ece449657 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java @@ -27,7 +27,6 @@ import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.saml.AdapterConstants; import org.keycloak.adapters.saml.DefaultSamlDeployment; import org.keycloak.adapters.saml.SamlConfigResolver; @@ -35,7 +34,17 @@ import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.SamlDeploymentContext; import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; import org.keycloak.adapters.saml.config.parsers.ResourceLoader; +import org.keycloak.adapters.saml.elytron.infinispan.InfinispanSessionCacheIdMapperUpdater; +import org.keycloak.adapters.spi.InMemorySessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.saml.common.exceptions.ParsingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Objects; /** *

A {@link ServletContextListener} that parses the keycloak adapter configuration and set the same configuration @@ -50,8 +59,14 @@ public class KeycloakConfigurationServletListener implements ServletContextListe protected static Logger log = Logger.getLogger(KeycloakConfigurationServletListener.class); - static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE = SamlDeploymentContext.class.getName(); - static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON = SamlDeploymentContext.class.getName() + ".elytron"; + public static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE = SamlDeploymentContext.class.getName(); + public static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON = SamlDeploymentContext.class.getName() + ".elytron"; + public static final String ADAPTER_SESSION_ID_MAPPER_ATTRIBUTE_ELYTRON = SessionIdMapper.class.getName() + ".elytron"; + public static final String ADAPTER_SESSION_ID_MAPPER_UPDATER_ATTRIBUTE_ELYTRON = SessionIdMapperUpdater.class.getName() + ".elytron"; + + private final SessionIdMapper idMapper = new InMemorySessionIdMapper(); + private SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT; + private Collection toClose = new LinkedList<>(); @Override public void contextInitialized(ServletContextEvent sce) { @@ -93,13 +108,23 @@ public class KeycloakConfigurationServletListener implements ServletContextListe } } + addTokenStoreUpdaters(servletContext); + servletContext.setAttribute(ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE, deploymentContext); servletContext.setAttribute(ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON, deploymentContext); + servletContext.setAttribute(ADAPTER_SESSION_ID_MAPPER_ATTRIBUTE_ELYTRON, idMapper); + servletContext.setAttribute(ADAPTER_SESSION_ID_MAPPER_UPDATER_ATTRIBUTE_ELYTRON, idMapperUpdater); } @Override public void contextDestroyed(ServletContextEvent sce) { - + for (AutoCloseable c : toClose) { + try { + c.close(); + } catch (Exception e) { + log.warnf(e, "Exception while destroying servlet context"); + } + } } private static InputStream getConfigInputStream(ServletContext context) { @@ -127,4 +152,64 @@ public class KeycloakConfigurationServletListener implements ServletContextListe } return new ByteArrayInputStream(json.getBytes()); } + + public void addTokenStoreUpdaters(ServletContext servletContext) { + SessionIdMapperUpdater updater = this.idMapperUpdater; + + try { + String idMapperSessionUpdaterClasses = servletContext.getInitParameter("keycloak.sessionIdMapperUpdater.classes"); + if (idMapperSessionUpdaterClasses == null) { + return; + } + + servletContext.addListener(new IdMapperUpdaterSessionListener(idMapper)); // This takes care of HTTP sessions manipulated locally + + updater = SessionIdMapperUpdater.DIRECT; + + for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) { + if (! clazz.isEmpty()) { + if (Objects.equals("org.keycloak.adapters.saml.wildfly.infinispan.InfinispanSessionCacheIdMapperUpdater", clazz)) { + clazz = InfinispanSessionCacheIdMapperUpdater.class.getName(); // exchange wildfly/undertow for elytron one + } + updater = invokeAddTokenStoreUpdaterMethod(clazz, servletContext, updater); + if (updater instanceof AutoCloseable) { + toClose.add((AutoCloseable) updater); + } + } + } + } finally { + setIdMapperUpdater(updater); + } + } + + private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, ServletContext servletContext, + SessionIdMapperUpdater previousIdMapperUpdater) { + try { + Class clazz = servletContext.getClassLoader().loadClass(idMapperSessionUpdaterClass); + Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", ServletContext.class, SessionIdMapper.class, SessionIdMapperUpdater.class); + if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers()) + || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers()) + || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) { + log.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } + + log.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, servletContext, idMapper, previousIdMapperUpdater); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) { + log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } + } + + public SessionIdMapperUpdater getIdMapperUpdater() { + return idMapperUpdater; + } + + protected void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) { + this.idMapperUpdater = idMapperUpdater; + } } diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java index 4d4c830abd..02b63edb33 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java @@ -31,7 +31,9 @@ import org.keycloak.adapters.saml.SamlDeploymentContext; import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpScope; import org.wildfly.security.http.HttpServerAuthenticationMechanism; import org.wildfly.security.http.HttpServerRequest; import org.wildfly.security.http.Scope; @@ -48,12 +50,14 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat private final CallbackHandler callbackHandler; private final SamlDeploymentContext deploymentContext; private final SessionIdMapper idMapper; + private final SessionIdMapperUpdater idMapperUpdater; - public KeycloakHttpServerAuthenticationMechanism(Map properties, CallbackHandler callbackHandler, SamlDeploymentContext deploymentContext, SessionIdMapper idMapper) { + public KeycloakHttpServerAuthenticationMechanism(Map properties, CallbackHandler callbackHandler, SamlDeploymentContext deploymentContext, SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater) { this.properties = properties; this.callbackHandler = callbackHandler; this.deploymentContext = deploymentContext; this.idMapper = idMapper; + this.idMapperUpdater = idMapperUpdater; } @Override @@ -72,7 +76,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat return; } - ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, idMapper, deploymentContext, callbackHandler); + ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, getSessionIdMapper(request), getSessionIdMapperUpdater(request), deploymentContext, callbackHandler); SamlDeployment deployment = httpFacade.getDeployment(); if (!deployment.isConfigured()) { @@ -138,6 +142,18 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat return this.deploymentContext; } + private SessionIdMapper getSessionIdMapper(HttpServerRequest request) { + HttpScope scope = request.getScope(Scope.APPLICATION); + SessionIdMapper res = scope == null ? null : (SessionIdMapper) scope.getAttachment(KeycloakConfigurationServletListener.ADAPTER_SESSION_ID_MAPPER_ATTRIBUTE_ELYTRON); + return res == null ? this.idMapper : res; + } + + private SessionIdMapperUpdater getSessionIdMapperUpdater(HttpServerRequest request) { + HttpScope scope = request.getScope(Scope.APPLICATION); + SessionIdMapperUpdater res = scope == null ? null : (SessionIdMapperUpdater) scope.getAttachment(KeycloakConfigurationServletListener.ADAPTER_SESSION_ID_MAPPER_UPDATER_ATTRIBUTE_ELYTRON); + return res == null ? this.idMapperUpdater : res; + } + protected void redirectLogout(SamlDeployment deployment, ElytronHttpFacade exchange) { sendRedirect(exchange, deployment.getLogoutPage()); } diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java index c1b69a4435..031a425f5a 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java @@ -25,6 +25,8 @@ import javax.security.auth.callback.CallbackHandler; import org.keycloak.adapters.saml.SamlDeploymentContext; import org.keycloak.adapters.spi.InMemorySessionIdMapper; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; +import org.jboss.logging.Logger; import org.wildfly.security.http.HttpAuthenticationException; import org.wildfly.security.http.HttpServerAuthenticationMechanism; import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; @@ -34,7 +36,7 @@ import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; */ public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpServerAuthenticationMechanismFactory { - private SessionIdMapper idMapper = new InMemorySessionIdMapper(); + private final SessionIdMapper idMapper = new InMemorySessionIdMapper(); private final SamlDeploymentContext deploymentContext; /** @@ -62,7 +64,8 @@ public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpSer mechanismProperties.putAll(properties); if (KeycloakHttpServerAuthenticationMechanism.NAME.equals(mechanismName)) { - return new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext, idMapper); + KeycloakHttpServerAuthenticationMechanism mech = new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext, idMapper, SessionIdMapperUpdater.DIRECT); + return mech; } return null; diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java new file mode 100644 index 0000000000..ba9f7b231d --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java @@ -0,0 +1,130 @@ +/* + * Copyright 2017 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.adapters.saml.elytron.infinispan; + +import org.keycloak.adapters.saml.AdapterConstants; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; + +import java.util.*; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import org.infinispan.Cache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.persistence.manager.PersistenceManager; +import org.infinispan.persistence.remote.RemoteStore; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +public class InfinispanSessionCacheIdMapperUpdater { + + private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class); + + public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container"; + + public static SessionIdMapperUpdater addTokenStoreUpdaters(ServletContext servletContext, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) { + String containerName = servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME); + String cacheName = servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME); + + // the following is based on https://github.com/jbossas/jboss-as/blob/7.2.0.Final/clustering/web-infinispan/src/main/java/org/jboss/as/clustering/web/infinispan/DistributedCacheManagerFactory.java#L116-L122 + String contextPath = servletContext.getContextPath(); + if (contextPath == null || contextPath.isEmpty() || "/".equals(contextPath)) { + contextPath = "/ROOT"; + } + String deploymentSessionCacheName = contextPath; + + if (containerName == null || cacheName == null) { + LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", contextPath); + + return previousIdMapperUpdater; + } + + String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName; + + try { + EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); + + Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName); + if (ssoCacheConfiguration == null) { + Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName); + if (cacheConfiguration == null) { + LOG.debugv("Using default configuration for SSO cache {0}.{1}.", containerName, cacheName); + ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration(); + } else { + LOG.debugv("Using distributed HTTP session cache configuration for SSO cache {0}.{1}, configuration taken from cache {2}", + containerName, cacheName, deploymentSessionCacheName); + ssoCacheConfiguration = cacheConfiguration; + cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration); + } + } else { + LOG.debugv("Using custom configuration of SSO cache {0}.{1}.", containerName, cacheName); + } + + CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode(); + if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) { + LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead.", ssoCacheConfiguration.clustering().cacheModeString()); + } + + Cache ssoCache = cacheManager.getCache(cacheName, true); + final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper); + ssoCache.addListener(listener); + + addSsoCacheCrossDcListener(ssoCache, listener); + + LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName); + + SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater) { + @Override + public void close() throws Exception { + ssoCache.stop(); + } + }; + + return updater; + } catch (NamingException ex) { + LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup); + return previousIdMapperUpdater; + } + } + + private static void addSsoCacheCrossDcListener(Cache ssoCache, SsoSessionCacheListener listener) { + if (ssoCache.getCacheConfiguration().persistence() == null) { + return; + } + + final Set stores = getRemoteStores(ssoCache); + if (stores == null || stores.isEmpty()) { + return; + } + + LOG.infov("Listening for events on remote stores configured for cache {0}", ssoCache.getName()); + + for (RemoteStore store : stores) { + store.getRemoteCache().addClientListener(listener); + } + } + + public static Set getRemoteStores(Cache ispnCache) { + return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java new file mode 100644 index 0000000000..23cbba0b58 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 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.adapters.saml.elytron.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; + +import org.infinispan.Cache; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +public abstract class SsoCacheSessionIdMapperUpdater implements SessionIdMapperUpdater, AutoCloseable { + + private static final Logger LOG = Logger.getLogger(SsoCacheSessionIdMapperUpdater.class.getName()); + + private final SessionIdMapperUpdater delegate; + /** + * Cache where key is a HTTP session ID, and value is a pair (user session ID, principal name) of Strings. + */ + private final Cache httpSessionToSsoCache; + + public SsoCacheSessionIdMapperUpdater(Cache httpSessionToSsoCache, SessionIdMapperUpdater previousIdMapperUpdater) { + this.delegate = previousIdMapperUpdater; + this.httpSessionToSsoCache = httpSessionToSsoCache; + } + + // SessionIdMapperUpdater methods + + @Override + public void clear(SessionIdMapper idMapper) { + httpSessionToSsoCache.clear(); + this.delegate.clear(idMapper); + } + + @Override + public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { + LOG.debugf("Adding mapping (%s, %s, %s)", sso, principal, httpSessionId); + + httpSessionToSsoCache.put(httpSessionId, new String[] {sso, principal}); + this.delegate.map(idMapper, sso, principal, httpSessionId); + } + + @Override + public void removeSession(SessionIdMapper idMapper, String httpSessionId) { + LOG.debugf("Removing session %s", httpSessionId); + + httpSessionToSsoCache.remove(httpSessionId); + this.delegate.removeSession(idMapper, httpSessionId); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java new file mode 100644 index 0000000000..84422860a4 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java @@ -0,0 +1,212 @@ +/* + * Copyright 2017 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.adapters.saml.elytron.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; + +import java.util.*; +import java.util.concurrent.*; +import org.infinispan.Cache; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved; +import org.infinispan.client.hotrod.annotation.ClientListener; +import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachelistener.annotation.*; +import org.infinispan.notifications.cachelistener.event.*; +import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStarted; +import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStopped; +import org.infinispan.notifications.cachemanagerlistener.event.CacheStartedEvent; +import org.infinispan.notifications.cachemanagerlistener.event.CacheStoppedEvent; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +@Listener(sync = false) +@ClientListener() +public class SsoSessionCacheListener { + + private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class); + + private final ConcurrentMap> map = new ConcurrentHashMap<>(); + + private final SessionIdMapper idMapper; + + private final Cache ssoCache; + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + public SsoSessionCacheListener(Cache ssoCache, SessionIdMapper idMapper) { + this.ssoCache = ssoCache; + this.idMapper = idMapper; + } + + @TransactionRegistered + public void startTransaction(TransactionRegisteredEvent event) { + if (event.getGlobalTransaction() == null) { + return; + } + + map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue<>()); + } + + @CacheStarted + public void cacheStarted(CacheStartedEvent event) { + this.executor = Executors.newSingleThreadExecutor(); + } + + @CacheStopped + public void cacheStopped(CacheStoppedEvent event) { + this.executor.shutdownNow(); + } + + @CacheEntryCreated + @CacheEntryRemoved + public void addEvent(TransactionalEvent event) { + if (event.isOriginLocal()) { + // Local events are processed by local HTTP session listener + return; + } + + if (event.isPre()) { // only handle post events + return; + } + + if (event.getGlobalTransaction() != null) { + map.get(event.getGlobalTransaction().globalId()).add(event); + } else { + processEvent(event); + } + } + + @TransactionCompleted + public void endTransaction(TransactionCompletedEvent event) { + if (event.getGlobalTransaction() == null) { + return; + } + + Queue events = map.remove(event.getGlobalTransaction().globalId()); + + if (events == null || ! event.isTransactionSuccessful()) { + return; + } + + for (final Event e : events) { + processEvent(e); + } + } + + private void processEvent(final Event e) { + switch (e.getType()) { + case CACHE_ENTRY_CREATED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryCreated((CacheEntryCreatedEvent) e); + } + }); + break; + + case CACHE_ENTRY_REMOVED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryRemoved((CacheEntryRemovedEvent) e); + } + }); + break; + } + } + + private void cacheEntryCreated(CacheEntryCreatedEvent event) { + if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) { + return; + } + String httpSessionId = (String) event.getKey(); + String[] value = (String[]) event.getValue(); + String ssoId = value[0]; + String principal = value[1]; + + LOG.tracev("cacheEntryCreated {0}:{1}", httpSessionId, ssoId); + + this.idMapper.map(ssoId, principal, httpSessionId); + } + + private void cacheEntryRemoved(CacheEntryRemovedEvent event) { + if (! (event.getKey() instanceof String)) { + return; + } + + LOG.tracev("cacheEntryRemoved {0}", event.getKey()); + + this.idMapper.removeSession((String) event.getKey()); + } + + @ClientCacheEntryCreated + public void remoteCacheEntryCreated(ClientCacheEntryCreatedEvent event) { + if (! (event.getKey() instanceof String)) { + return; + } + + String httpSessionId = (String) event.getKey(); + + if (idMapper.hasSession(httpSessionId)) { + // Ignore local events generated by remote store + LOG.tracev("IGNORING remoteCacheEntryCreated {0}", httpSessionId); + return; + } + + this.executor.submit(new Runnable() { + + @Override + public void run() { + String[] value; + try { + value = ssoCache.get((String) httpSessionId); + + if (value != null) { + String ssoId = value[0]; + String principal = value[1]; + + LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId); + + idMapper.map(ssoId, principal, httpSessionId); + } else { + LOG.tracev("remoteCacheEntryCreated {0}", event.getKey()); + + } + } catch (Exception ex) { + LOG.debugf(ex, "Cannot get remote cache entry %s", httpSessionId); + } + } + }); + } + + @ClientCacheEntryRemoved + public void remoteCacheEntryRemoved(ClientCacheEntryRemovedEvent event) { + LOG.tracev("remoteCacheEntryRemoved {0}", event.getKey()); + + this.executor.submit(new Runnable() { + + @Override + public void run() { + idMapper.removeSession((String) event.getKey()); + } + }); + } +} diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java index 6d53485aa5..fc2a1e013a 100644 --- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java @@ -171,19 +171,25 @@ public class SsoSessionCacheListener { return; } - String[] value = ssoCache.get((String) httpSessionId); + this.executor.submit(new Runnable() { - if (value != null) { - String ssoId = value[0]; - String principal = value[1]; + @Override + public void run() { + String[] value = ssoCache.get((String) httpSessionId); - LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId); + if (value != null) { + String ssoId = value[0]; + String principal = value[1]; - this.idMapper.map(ssoId, principal, httpSessionId); - } else { - LOG.tracev("remoteCacheEntryCreated {0}", event.getKey()); + LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId); - } + idMapper.map(ssoId, principal, httpSessionId); + } else { + LOG.tracev("remoteCacheEntryCreated {0}", event.getKey()); + + } + } + }); } @ClientCacheEntryRemoved diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java index 7ca8af6e70..dfbe846210 100755 --- a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java +++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jboss.logging.Logger; /** * Maps external principal and SSO id to internal local http session id @@ -29,6 +30,9 @@ import java.util.concurrent.ConcurrentHashMap; * @version $Revision: 1 $ */ public class InMemorySessionIdMapper implements SessionIdMapper { + + private static final Logger LOG = Logger.getLogger(InMemorySessionIdMapper.class.getName()); + ConcurrentHashMap ssoToSession = new ConcurrentHashMap<>(); ConcurrentHashMap sessionToSso = new ConcurrentHashMap<>(); ConcurrentHashMap> principalToSession = new ConcurrentHashMap<>(); @@ -63,6 +67,8 @@ public class InMemorySessionIdMapper implements SessionIdMapper { @Override public void map(String sso, String principal, String session) { + LOG.debugf("Adding mapping (%s, %s, %s)", sso, principal, session); + if (sso != null) { ssoToSession.put(sso, session); sessionToSso.put(session, sso); @@ -86,6 +92,8 @@ public class InMemorySessionIdMapper implements SessionIdMapper { @Override public void removeSession(String session) { + LOG.debugf("Removing session %s", session); + String sso = sessionToSso.remove(session); if (sso != null) { ssoToSession.remove(sso); diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli index 01e7ad958d..377ae72154 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli +++ b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli @@ -1,4 +1,5 @@ embed-server --server-config=${server.config:standalone.xml} /subsystem=logging/logger=org.keycloak.adapters:add(level=DEBUG) +/subsystem=logging/logger=org.keycloak.subsystem.adapter:add(level=DEBUG) /subsystem=logging/console-handler=CONSOLE:change-log-level(level=DEBUG) diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli index 223e4190b5..4e39dfaabe 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli +++ b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli @@ -25,4 +25,16 @@ embed-server --server-config=standalone-ha.xml /subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache/store=remote:add(remote-servers=[cache-server],cache=employee-distributable-cache,passivation=false,purge=false,preload=false,shared=true) /subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache.ssoCache:add(statistics-enabled=true,mode=SYNC) -/subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache.ssoCache/store=remote:add(remote-servers=[cache-server],cache=employee-distributable-cache.ssoCache,passivation=false,purge=false,preload=false,shared=true) +/subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache.ssoCache/store=remote:add( \ + remote-servers=["cache-server"], \ + cache=employee-distributable-cache.ssoCache, \ + passivation=false, \ + purge=false, \ + preload=false, \ + shared=true, \ + fetch-state=false, \ + properties={ \ + rawValues=true, \ + protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion:2.6} \ + } \ +) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java index a098af1fe4..68971e9aba 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java @@ -76,7 +76,7 @@ public class CrossDCTestEnricher { if (annotation == null) { Class annotatedClass = getNearestSuperclassWithAnnotation(event.getTestClass().getJavaClass(), InitialDcState.class); - annotation = annotatedClass.getAnnotation(InitialDcState.class); + annotation = annotatedClass == null ? null : annotatedClass.getAnnotation(InitialDcState.class); } if (annotation == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java index 576eadfcdb..94e99192e9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java @@ -120,13 +120,16 @@ public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsA Assume.assumeThat(PORT_OFFSET_NODE_1, not(is(-1))); Assume.assumeThat(PORT_OFFSET_NODE_2, not(is(-1))); Assume.assumeThat(PORT_OFFSET_NODE_REVPROXY, not(is(-1))); - assumeNotElytronAdapter(); } @Before public void prepareReverseProxy() throws Exception { loadBalancerToNodes = new LoadBalancingProxyClient().addHost(NODE_1_URI, NODE_1_NAME).setConnectionsPerThread(10); - reverseProxyToNodes = Undertow.builder().addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost").setIoThreads(2).setHandler(new ProxyHandler(loadBalancerToNodes, 5000, ResponseCodeHandler.HANDLE_404)).build(); + int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers + reverseProxyToNodes = Undertow.builder() + .addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost") + .setIoThreads(2) + .setHandler(new ProxyHandler(loadBalancerToNodes, maxTime, ResponseCodeHandler.HANDLE_404)).build(); reverseProxyToNodes.start(); } @@ -232,20 +235,6 @@ public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsA log.infov("Logged out via admin console"); } - private static void assumeNotElytronAdapter() { - if (!AppServerTestEnricher.isUndertowAppServer()) { - try { - boolean contains = FileUtils.readFileToString(Paths.get(System.getProperty("app.server.home"), "standalone", "configuration", "standalone.xml").toFile(), "UTF-8").contains("