diff --git a/adapters/saml/as7-eap6/adapter/pom.xml b/adapters/saml/as7-eap6/adapter/pom.xml index dd8aff5317..02f6deacd1 100755 --- a/adapters/saml/as7-eap6/adapter/pom.xml +++ b/adapters/saml/as7-eap6/adapter/pom.xml @@ -78,6 +78,18 @@ 7.1.2.Final provided + + org.infinispan + infinispan-core + provided + 5.2.20.Final + + + org.infinispan + infinispan-cachestore-remote + provided + 5.2.20.Final + org.keycloak keycloak-saml-tomcat-adapter-core diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java index b6f4c23f09..dd19a7b7e6 100644 --- a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java +++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java @@ -16,9 +16,11 @@ */ package org.keycloak.adapters.saml.jbossweb.infinispan; +import org.keycloak.adapters.saml.AdapterConstants; import org.keycloak.adapters.spi.SessionIdMapper; import org.keycloak.adapters.spi.SessionIdMapperUpdater; +import java.util.List; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.servlet.ServletContext; @@ -26,6 +28,8 @@ import org.apache.catalina.Context; import org.infinispan.Cache; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.Configuration; +import org.infinispan.loaders.CacheLoaderManager; +import org.infinispan.loaders.remote.RemoteCacheStore; import org.infinispan.manager.EmbeddedCacheManager; import org.jboss.logging.Logger; @@ -37,24 +41,12 @@ 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/web"; - - private static final String DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheContainerJndi"; - private static final String DEPLOYMENT_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.deploymentCacheName"; - private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName"; + public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container"; public static SessionIdMapperUpdater addTokenStoreUpdaters(Context context, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) { - boolean distributable = context.getDistributable(); - - if (! distributable) { - LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", context.getName()); - return previousIdMapperUpdater; - } - ServletContext servletContext = context.getServletContext(); - String cacheContainerLookup = (servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null) - ? servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) - : DEFAULT_CACHE_CONTAINER_JNDI_NAME; + String containerName = servletContext == null ? null : servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME); + String cacheName = servletContext == null ? null : 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 host = context.getParent() == null ? "" : context.getParent().getName(); @@ -62,43 +54,48 @@ public class InfinispanSessionCacheIdMapperUpdater { if ("/".equals(contextPath)) { contextPath = "/ROOT"; } + String deploymentSessionCacheName = host + contextPath; - boolean deploymentSessionCacheNamePreset = servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME) != null; - String deploymentSessionCacheName = deploymentSessionCacheNamePreset - ? servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME) - : host + contextPath; - boolean ssoCacheNamePreset = servletContext != null && servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME) != null; - String ssoCacheName = ssoCacheNamePreset - ? servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME) - : deploymentSessionCacheName + ".ssoCache"; + if (containerName == null || cacheName == null || deploymentSessionCacheName == null) { + LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", host + contextPath); + + return previousIdMapperUpdater; + } + + String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName; try { EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); - Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName); + Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName); if (ssoCacheConfiguration == null) { Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName); if (cacheConfiguration == null) { - LOG.debugv("Using default cache container configuration for SSO cache. lookup={0}, looked up configuration of cache={1}", cacheContainerLookup, deploymentSessionCacheName); + 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. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName); + 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(ssoCacheName, ssoCacheConfiguration); + cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration); } } else { - LOG.debugv("Using custom configuration for SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName); + 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()); + LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead.", ssoCacheConfiguration.clustering().cacheModeString()); } - Cache ssoCache = cacheManager.getCache(ssoCacheName, true); - ssoCache.addListener(new SsoSessionCacheListener(mapper)); + Cache ssoCache = cacheManager.getCache(cacheName, true); + final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper); + ssoCache.addListener(listener); - LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName); + // Not possible to add listener for cross-DC support because of too old Infinispan in AS 7 + warnIfRemoteStoreIsUsed(ssoCache); + + LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName); SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater); @@ -108,4 +105,17 @@ public class InfinispanSessionCacheIdMapperUpdater { return previousIdMapperUpdater; } } + + private static void warnIfRemoteStoreIsUsed(Cache ssoCache) { + final List stores = getRemoteStores(ssoCache); + if (stores == null || stores.isEmpty()) { + return; + } + + LOG.warnv("Unable to listen for events on remote stores configured for cache {0} (unsupported in this Infinispan limitations), logouts will not be propagated.", ssoCache.getName()); + } + + public static List getRemoteStores(Cache ssoCache) { + return ssoCache.getAdvancedCache().getComponentRegistry().getComponent(CacheLoaderManager.class).getCacheLoaders(RemoteCacheStore.class); + } } diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java index ee100ad317..aded4a38e9 100644 --- a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java +++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java @@ -20,6 +20,7 @@ import org.keycloak.adapters.spi.SessionIdMapper; import java.util.*; import java.util.concurrent.*; +import org.infinispan.Cache; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.annotation.*; import org.infinispan.notifications.cachelistener.event.*; @@ -43,9 +44,12 @@ public class SsoSessionCacheListener { private final SessionIdMapper idMapper; + private final Cache ssoCache; + private ExecutorService executor = Executors.newSingleThreadExecutor(); - public SsoSessionCacheListener(SessionIdMapper idMapper) { + public SsoSessionCacheListener(Cache ssoCache, SessionIdMapper idMapper) { + this.ssoCache = ssoCache; this.idMapper = idMapper; } @@ -68,8 +72,10 @@ public class SsoSessionCacheListener { @CacheEntryRemoved @CacheEntryModified public void addEvent(TransactionalEvent event) { - if (event.isPre() == false) { + if (event.getGlobalTransaction() != null) { map.get(event.getGlobalTransaction()).add(event); + } else { + processEvent(event); } } @@ -87,40 +93,53 @@ public class SsoSessionCacheListener { } for (final Event e : events) { - switch (e.getType()) { - case CACHE_ENTRY_CREATED: - this.executor.submit(new Runnable() { - @Override public void run() { - cacheEntryCreated((CacheEntryCreatedEvent) e); - } - }); - break; + processEvent(e); + } + } - case CACHE_ENTRY_MODIFIED: - this.executor.submit(new Runnable() { - @Override public void run() { - cacheEntryModified((CacheEntryModifiedEvent) e); - } - }); - break; + 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_MODIFIED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryModified((CacheEntryModifiedEvent) e); + } + }); + break; - case CACHE_ENTRY_REMOVED: - this.executor.submit(new Runnable() { - @Override public void run() { - cacheEntryRemoved((CacheEntryRemovedEvent) 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[])) { + if (! (event.getKey() instanceof String)) { return; } + String httpSessionId = (String) event.getKey(); - String[] value = (String[]) event.getValue(); + + if (idMapper.hasSession(httpSessionId)) { + // Ignore local events generated by remote store + LOG.tracev("IGNORING cacheEntryCreated {0}", httpSessionId); + return; + } + + String[] value = ssoCache.get((String) httpSessionId); + String ssoId = value[0]; String principal = value[1]; diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakClusteredSsoDeploymentProcessor.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakClusteredSsoDeploymentProcessor.java new file mode 100644 index 0000000000..0333bc9dee --- /dev/null +++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakClusteredSsoDeploymentProcessor.java @@ -0,0 +1,157 @@ +/* + * 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.subsystem.saml.as7; + +import org.keycloak.adapters.saml.AdapterConstants; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.jboss.as.server.deployment.DeploymentPhaseContext; +import org.jboss.as.server.deployment.DeploymentUnit; +import org.jboss.as.server.deployment.DeploymentUnitProcessingException; +import org.jboss.as.server.deployment.DeploymentUnitProcessor; +import org.jboss.as.web.deployment.WarMetaData; +import org.jboss.logging.Logger; +import org.jboss.metadata.javaee.spec.ParamValueMetaData; +import org.jboss.metadata.web.jboss.JBossWebMetaData; +import org.jboss.metadata.web.spec.LoginConfigMetaData; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.ServiceTarget; + +/** + * + * @author hmlnarik + */ +public class KeycloakClusteredSsoDeploymentProcessor implements DeploymentUnitProcessor { + + private static final Logger LOG = Logger.getLogger(KeycloakClusteredSsoDeploymentProcessor.class); + + private static final String DEFAULT_CACHE_CONTAINER = "web"; + private static final String SSO_CACHE_CONTAINER_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.containerName"; + private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName"; + + @Override + public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { + final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); + + if (isKeycloakSamlAuthMethod(deploymentUnit) && isDistributable(deploymentUnit)) { + addSamlReplicationConfiguration(deploymentUnit, phaseContext); + } + } + + public static boolean isDistributable(final DeploymentUnit deploymentUnit) { + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + return false; + } + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + return false; + } + + return webMetaData.getDistributable() != null || webMetaData.getReplicationConfig() != null; + } + + public static boolean isKeycloakSamlAuthMethod(final DeploymentUnit deploymentUnit) { + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + return false; + } + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + return false; + } + + if (Configuration.INSTANCE.isSecureDeployment(deploymentUnit)) { + return true; + } + + LoginConfigMetaData loginConfig = webMetaData.getLoginConfig(); + + return loginConfig != null && Objects.equals(loginConfig.getAuthMethod(), "KEYCLOAK-SAML"); + } + + @Override + public void undeploy(DeploymentUnit du) { + + } + + private void addSamlReplicationConfiguration(DeploymentUnit deploymentUnit, DeploymentPhaseContext context) { + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + return; + } + + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + webMetaData = new JBossWebMetaData(); + warMetaData.setMergedJBossWebMetaData(webMetaData); + } + + // Find out default names of cache container and cache + String cacheContainer = DEFAULT_CACHE_CONTAINER; + String deploymentSessionCacheName = + (deploymentUnit.getParent() == null + ? "" + : deploymentUnit.getParent().getName() + ".") + + deploymentUnit.getName(); + + // Update names from jboss-web.xml's + if (webMetaData.getReplicationConfig() != null && webMetaData.getReplicationConfig().getCacheName() != null) { + ServiceName sn = ServiceName.parse(webMetaData.getReplicationConfig().getCacheName()); + cacheContainer = sn.getParent().getSimpleName(); + deploymentSessionCacheName = sn.getSimpleName(); + } + String ssoCacheName = deploymentSessionCacheName + ".ssoCache"; + + // Override if they were set in the context parameters + List contextParams = webMetaData.getContextParams(); + if (contextParams == null) { + contextParams = new ArrayList<>(); + } + for (ParamValueMetaData contextParam : contextParams) { + if (Objects.equals(contextParam.getParamName(), SSO_CACHE_CONTAINER_NAME_PARAM_NAME)) { + cacheContainer = contextParam.getParamValue(); + } else if (Objects.equals(contextParam.getParamName(), SSO_CACHE_NAME_PARAM_NAME)) { + ssoCacheName = contextParam.getParamValue(); + } + } + + LOG.debugv("Determined SSO cache container configuration: container: {0}, cache: {1}", cacheContainer, ssoCacheName); +// addCacheDependency(context, deploymentUnit, cacheContainer, cacheName); + + // Set context parameters for SSO cache container/name + ParamValueMetaData paramContainer = new ParamValueMetaData(); + paramContainer.setParamName(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME); + paramContainer.setParamValue(cacheContainer); + contextParams.add(paramContainer); + + ParamValueMetaData paramSsoCache = new ParamValueMetaData(); + paramSsoCache.setParamName(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME); + paramSsoCache.setParamValue(ssoCacheName); + contextParams.add(paramSsoCache); + + webMetaData.setContextParams(contextParams); + } + + private void addCacheDependency(DeploymentPhaseContext context, DeploymentUnit deploymentUnit, String cacheContainer, String cacheName) { + ServiceName jbossAsCacheContainerService = ServiceName.of("jboss", "infinispan", cacheContainer); + ServiceTarget st = context.getServiceTarget(); + st.addDependency(jbossAsCacheContainerService.append(cacheName)); + } + +} diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java index d583db43d1..30a853ffa6 100755 --- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java +++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java @@ -48,6 +48,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { Phase.POST_MODULE, // PHASE Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY chooseConfigDeploymentProcessor()); + processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME, + Phase.POST_MODULE, // PHASE + Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY + chooseClusteredSsoDeploymentProcessor()); } }, OperationContext.Stage.RUNTIME); } @@ -60,6 +64,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { return new KeycloakAdapterConfigDeploymentProcessor(); } + private DeploymentUnitProcessor chooseClusteredSsoDeploymentProcessor() { + return new KeycloakClusteredSsoDeploymentProcessor(); + } + @Override protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java index 8b94068229..3646ed4554 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java @@ -23,4 +23,6 @@ package org.keycloak.adapters.saml; */ public class AdapterConstants { public static final String AUTH_DATA_PARAM_NAME="org.keycloak.saml.xml.adapterConfig"; + public static final String REPLICATION_CONFIG_CONTAINER_PARAM_NAME = "org.keycloak.saml.replication.container"; + public static final String REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME = "org.keycloak.saml.replication.cache.sso"; } diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java index 2bf2369ef1..7e8fb83456 100755 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java +++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java @@ -152,12 +152,19 @@ public class ServletSamlSessionStore implements SamlSessionStore { public boolean isLoggedIn() { HttpSession session = getSession(false); if (session == null) { - log.debug("session was null, returning null"); + log.debug("Session was not found"); return false; } + + if (! idMapper.hasSession(session.getId())) { + log.debugf("Session %s has expired on some other node", session.getId()); + session.removeAttribute(SamlSession.class.getName()); + return false; + } + final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); if (samlSession == null) { - log.debug("SamlSession was not in session, returning null"); + log.debug("SamlSession was not found in the session"); return false; } diff --git a/adapters/saml/wildfly/wildfly-adapter/pom.xml b/adapters/saml/wildfly/wildfly-adapter/pom.xml index 3be5e7e4c0..73135d55ea 100755 --- a/adapters/saml/wildfly/wildfly-adapter/pom.xml +++ b/adapters/saml/wildfly/wildfly-adapter/pom.xml @@ -70,6 +70,10 @@ org.infinispan infinispan-core + + org.infinispan + infinispan-cachestore-remote + org.picketbox picketbox diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java index 489d1d5c67..c35db63a1f 100644 --- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java @@ -16,6 +16,7 @@ */ package org.keycloak.adapters.saml.wildfly.infinispan; +import org.keycloak.adapters.saml.AdapterConstants; import org.keycloak.adapters.spi.SessionIdMapper; import org.keycloak.adapters.spi.SessionIdMapperUpdater; @@ -27,6 +28,8 @@ 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; /** @@ -37,64 +40,55 @@ 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/web"; - - private static final String DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheContainerJndi"; - private static final String DEPLOYMENT_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.deploymentCacheName"; - private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName"; + public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container"; public static SessionIdMapperUpdater addTokenStoreUpdaters(DeploymentInfo deploymentInfo, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) { - boolean distributable = Objects.equals( - deploymentInfo.getSessionManagerFactory().getClass().getName(), - "org.wildfly.clustering.web.undertow.session.DistributableSessionManagerFactory" - ); + Map initParameters = deploymentInfo.getInitParameters(); + String containerName = initParameters == null ? null : initParameters.get(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME); + String cacheName = initParameters == null ? null : initParameters.get(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME); + + if (containerName == null || cacheName == null) { + LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", deploymentInfo.getDeploymentName()); - if (! distributable) { - LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", deploymentInfo.getDeploymentName()); return previousIdMapperUpdater; } - Map initParameters = deploymentInfo.getInitParameters(); - String cacheContainerLookup = (initParameters != null && initParameters.get(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null) - ? initParameters.get(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) - : DEFAULT_CACHE_CONTAINER_JNDI_NAME; - boolean deploymentSessionCacheNamePreset = initParameters != null && initParameters.get(DEPLOYMENT_CACHE_NAME_PARAM_NAME) != null; - String deploymentSessionCacheName = deploymentSessionCacheNamePreset - ? initParameters.get(DEPLOYMENT_CACHE_NAME_PARAM_NAME) - : deploymentInfo.getDeploymentName(); - boolean ssoCacheNamePreset = initParameters != null && initParameters.get(SSO_CACHE_NAME_PARAM_NAME) != null; - String ssoCacheName = ssoCacheNamePreset - ? initParameters.get(SSO_CACHE_NAME_PARAM_NAME) - : deploymentSessionCacheName + ".ssoCache"; + String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName; + String deploymentSessionCacheName = deploymentInfo.getDeploymentName(); try { EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); - Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName); + Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName); if (ssoCacheConfiguration == null) { Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName); if (cacheConfiguration == null) { - LOG.debugv("Using default cache container configuration for SSO cache. lookup={0}, looked up configuration of cache={1}", cacheContainerLookup, deploymentSessionCacheName); + 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. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName); + 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(ssoCacheName, ssoCacheConfiguration); + cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration); } } else { - LOG.debugv("Using custom configuration of SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName); + 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()); + LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead.", ssoCacheConfiguration.clustering().cacheModeString()); } - Cache ssoCache = cacheManager.getCache(ssoCacheName, true); - ssoCache.addListener(new SsoSessionCacheListener(mapper)); + Cache ssoCache = cacheManager.getCache(cacheName, true); + final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper); + ssoCache.addListener(listener); - LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName); + addSsoCacheCrossDcListener(ssoCache, listener); + LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName); + + LOG.debugv("Adding session listener for SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName); SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater); deploymentInfo.addSessionListener(updater); @@ -104,4 +98,25 @@ public class InfinispanSessionCacheIdMapperUpdater { 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/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 ccd102e713..6d53485aa5 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 @@ -20,6 +20,12 @@ 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.*; @@ -34,6 +40,7 @@ import org.jboss.logging.Logger; * @author hmlnarik */ @Listener +@ClientListener public class SsoSessionCacheListener { private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class); @@ -42,14 +49,21 @@ public class SsoSessionCacheListener { private final SessionIdMapper idMapper; + private final Cache ssoCache; + private ExecutorService executor = Executors.newSingleThreadExecutor(); - public SsoSessionCacheListener(SessionIdMapper idMapper) { + 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()); } @@ -66,42 +80,56 @@ public class SsoSessionCacheListener { @CacheEntryCreated @CacheEntryRemoved public void addEvent(TransactionalEvent event) { - if (event.isPre() == false) { + 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; } - if (event.isOriginLocal()) { - // Local events are processed by local HTTP session listener - return; - } - for (final Event e : events) { - switch (e.getType()) { - case CACHE_ENTRY_CREATED: - this.executor.submit(new Runnable() { - @Override public void run() { - cacheEntryCreated((CacheEntryCreatedEvent) e); - } - }); - break; + processEvent(e); + } + } - case CACHE_ENTRY_REMOVED: - this.executor.submit(new Runnable() { - @Override public void run() { - cacheEntryRemoved((CacheEntryRemovedEvent) e); - } - }); - break; - } + 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; } } @@ -128,4 +156,40 @@ public class SsoSessionCacheListener { 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; + } + + String[] value = ssoCache.get((String) httpSessionId); + + if (value != null) { + String ssoId = value[0]; + String principal = value[1]; + + LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId); + + this.idMapper.map(ssoId, principal, httpSessionId); + } else { + LOG.tracev("remoteCacheEntryCreated {0}", event.getKey()); + + } + } + + @ClientCacheEntryRemoved + public void remoteCacheEntryRemoved(ClientCacheEntryRemovedEvent event) { + LOG.tracev("remoteCacheEntryRemoved {0}", event.getKey()); + + this.idMapper.removeSession((String) event.getKey()); + } } diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakClusteredSsoDeploymentProcessor.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakClusteredSsoDeploymentProcessor.java new file mode 100644 index 0000000000..3be66deb79 --- /dev/null +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakClusteredSsoDeploymentProcessor.java @@ -0,0 +1,178 @@ +/* + * 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.subsystem.adapter.saml.extension; + +import org.keycloak.adapters.saml.AdapterConstants; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.jboss.as.controller.capability.CapabilityServiceSupport; +import org.jboss.as.server.deployment.Attachments; +import org.jboss.as.server.deployment.DeploymentPhaseContext; +import org.jboss.as.server.deployment.DeploymentUnit; +import org.jboss.as.server.deployment.DeploymentUnitProcessingException; +import org.jboss.as.server.deployment.DeploymentUnitProcessor; +import org.jboss.as.web.common.WarMetaData; +import org.jboss.logging.Logger; +import org.jboss.metadata.javaee.spec.ParamValueMetaData; +import org.jboss.metadata.web.jboss.JBossWebMetaData; +import org.jboss.metadata.web.spec.LoginConfigMetaData; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.ServiceTarget; + +/** + * + * @author hmlnarik + */ +public class KeycloakClusteredSsoDeploymentProcessor implements DeploymentUnitProcessor { + + private static final Logger LOG = Logger.getLogger(KeycloakClusteredSsoDeploymentProcessor.class); + + private static final String DEFAULT_CACHE_CONTAINER = "web"; + private static final String SSO_CACHE_CONTAINER_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.containerName"; + private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName"; + + @Override + public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { + final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); + + if (isKeycloakSamlAuthMethod(deploymentUnit) && isDistributable(deploymentUnit)) { + addSamlReplicationConfiguration(deploymentUnit, phaseContext); + } + } + + public static boolean isDistributable(final DeploymentUnit deploymentUnit) { + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + return false; + } + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + return false; + } + + return webMetaData.getDistributable() != null || webMetaData.getReplicationConfig() != null; + } + + public static boolean isKeycloakSamlAuthMethod(final DeploymentUnit deploymentUnit) { + if (Configuration.INSTANCE.getSecureDeployment(deploymentUnit) != null) { + return true; + } + + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + return false; + } + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + return false; + } + + LoginConfigMetaData loginConfig = webMetaData.getLoginConfig(); + + return loginConfig != null && Objects.equals(loginConfig.getAuthMethod(), "KEYCLOAK-SAML"); + } + + @Override + public void undeploy(DeploymentUnit du) { + + } + + private void addSamlReplicationConfiguration(DeploymentUnit deploymentUnit, DeploymentPhaseContext context) { + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + return; + } + + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + webMetaData = new JBossWebMetaData(); + warMetaData.setMergedJBossWebMetaData(webMetaData); + } + + // Find out default names of cache container and cache + String cacheContainer = DEFAULT_CACHE_CONTAINER; + String deploymentSessionCacheName = + (deploymentUnit.getParent() == null + ? "" + : deploymentUnit.getParent().getName() + ".") + + deploymentUnit.getName(); + + // Update names from jboss-web.xml's + if (webMetaData.getReplicationConfig() != null && webMetaData.getReplicationConfig().getCacheName() != null) { + ServiceName sn = ServiceName.parse(webMetaData.getReplicationConfig().getCacheName()); + cacheContainer = sn.getParent().getSimpleName(); + deploymentSessionCacheName = sn.getSimpleName(); + } + String ssoCacheName = deploymentSessionCacheName + ".ssoCache"; + + // Override if they were set in the context parameters + List contextParams = webMetaData.getContextParams(); + if (contextParams == null) { + contextParams = new ArrayList<>(); + } + for (ParamValueMetaData contextParam : contextParams) { + if (Objects.equals(contextParam.getParamName(), SSO_CACHE_CONTAINER_NAME_PARAM_NAME)) { + cacheContainer = contextParam.getParamValue(); + } else if (Objects.equals(contextParam.getParamName(), SSO_CACHE_NAME_PARAM_NAME)) { + ssoCacheName = contextParam.getParamValue(); + } + } + + LOG.debugv("Determined SSO cache container configuration: container: {0}, cache: {1}", cacheContainer, ssoCacheName); + addCacheDependency(context, deploymentUnit, cacheContainer, ssoCacheName); + + // Set context parameters for SSO cache container/name + ParamValueMetaData paramContainer = new ParamValueMetaData(); + paramContainer.setParamName(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME); + paramContainer.setParamValue(cacheContainer); + contextParams.add(paramContainer); + + ParamValueMetaData paramSsoCache = new ParamValueMetaData(); + paramSsoCache.setParamName(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME); + paramSsoCache.setParamValue(ssoCacheName); + contextParams.add(paramSsoCache); + + webMetaData.setContextParams(contextParams); + } + + private void addCacheDependency(DeploymentPhaseContext context, DeploymentUnit deploymentUnit, String cacheContainer, String cacheName) { + ServiceName wf10CacheContainerServiceName = ServiceName.of("jboss", "infinispan", cacheContainer); + final ServiceController wf10CacheContainerService = context.getServiceRegistry().getService(wf10CacheContainerServiceName); + + boolean legacy = wf10CacheContainerService != null; + ServiceTarget st = context.getServiceTarget(); + + if (legacy) { + ServiceName cacheServiceName = wf10CacheContainerServiceName.append(cacheName); + ServiceController cacheService = context.getServiceRegistry().getService(cacheServiceName); + if (cacheService != null) { + st.addDependency(cacheServiceName); + } + } else { + CapabilityServiceSupport support = deploymentUnit.getAttachment(Attachments.CAPABILITY_SERVICE_SUPPORT); + + ServiceName cacheServiceName = support.getCapabilityServiceName("org.wildfly.clustering.infinispan.cache." + cacheContainer + "." + cacheName); + ServiceController cacheService = context.getServiceRegistry().getService(cacheServiceName); + if (cacheService != null) { + st.addDependency(cacheServiceName); + } + } + } + +} diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java index 79a49812d3..e9ef1a3ee1 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java @@ -43,6 +43,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { Phase.POST_MODULE, // PHASE Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY chooseConfigDeploymentProcessor()); + processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME, + Phase.POST_MODULE, // PHASE + Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY + chooseClusteredSsoDeploymentProcessor()); } }, OperationContext.Stage.RUNTIME); } @@ -54,4 +58,8 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { private DeploymentUnitProcessor chooseConfigDeploymentProcessor() { return new KeycloakAdapterConfigDeploymentProcessor(); } + + private DeploymentUnitProcessor chooseClusteredSsoDeploymentProcessor() { + return new KeycloakClusteredSsoDeploymentProcessor(); + } } 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 a00ae829f0..7ca8af6e70 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 @@ -67,6 +67,11 @@ public class InMemorySessionIdMapper implements SessionIdMapper { ssoToSession.put(sso, session); sessionToSso.put(session, sso); } + + if (principal == null) { + return; + } + Set userSessions = principalToSession.get(principal); if (userSessions == null) { final Set tmp = Collections.synchronizedSet(new HashSet()); diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml index fd9d2e4089..885470fb2e 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml @@ -34,7 +34,6 @@ - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml index 857a8e31b8..b61266df6c 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml @@ -40,5 +40,6 @@ + diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl index b6fbd2e53c..9a68ecf0e3 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl @@ -39,6 +39,8 @@ + + @@ -57,6 +59,8 @@ + + diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java index a17a75a850..257afc0db0 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java @@ -51,6 +51,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -289,6 +290,18 @@ public class SamlClient { } } + public void execute(Step... steps) { + executeAndTransform(resp -> null, Arrays.asList(steps)); + } + + public void execute(List steps) { + executeAndTransform(resp -> null, steps); + } + + public T executeAndTransform(ResultExtractor resultTransformer, Step... steps) { + return executeAndTransform(resultTransformer, Arrays.asList(steps)); + } + public T executeAndTransform(ResultExtractor resultTransformer, List steps) { CloseableHttpResponse currentResponse = null; URI currentUri = URI.create("about:blank"); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java index 89d309249c..3879447fb5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java @@ -33,6 +33,10 @@ import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder; import org.keycloak.testsuite.util.saml.LoginBuilder; import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder; import org.keycloak.testsuite.util.saml.RequiredConsentBuilder; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.hamcrest.Matcher; +import org.junit.Assert; import org.w3c.dom.Document; /** @@ -43,6 +47,19 @@ public class SamlClientBuilder { private final List steps = new LinkedList<>(); + /** + * Execute the current steps without any work on the final response. + * @return Client that executed the steps + */ + public SamlClient execute() { + return execute(resp -> {}); + } + + /** + * Execute the current steps and pass the final response to the {@code resultConsumer} for processing. + * @param resultConsumer This function is given the final response + * @return Client that executed the steps + */ public SamlClient execute(Consumer resultConsumer) { final SamlClient samlClient = new SamlClient(); samlClient.executeAndTransform(r -> { @@ -52,6 +69,11 @@ public class SamlClientBuilder { return samlClient; } + /** + * Execute the current steps and pass the final response to the {@code resultTransformer} for processing. + * @param resultTransformer This function is given the final response and processes it into some value + * @return Value returned by {@code resultTransformer} + */ public T executeAndTransform(ResultExtractor resultTransformer) { return new SamlClient().executeAndTransform(resultTransformer, steps); } @@ -60,11 +82,48 @@ public class SamlClientBuilder { return steps; } - public T addStep(T step) { + public T addStepBuilder(T step) { steps.add(step); return step; } + /** + * Adds a single generic step + * @param step + * @return This builder + */ + public SamlClientBuilder addStep(Step step) { + steps.add(step); + return this; + } + + /** + * Adds a single generic step + * @param step + * @return This builder + */ + public SamlClientBuilder addStep(Runnable stepWithNoParameters) { + addStep((client, currentURI, currentResponse, context) -> { + stepWithNoParameters.run(); + return null; + }); + return this; + } + + public SamlClientBuilder assertResponse(Matcher matcher) { + steps.add((client, currentURI, currentResponse, context) -> { + Assert.assertThat(currentResponse, matcher); + return null; + }); + return this; + } + + /** + * When executing the {@link HttpUriRequest} obtained from the previous step, + * do not to follow HTTP redirects but pass the first response immediately + * to the following step. + * @return This builder + */ public SamlClientBuilder doNotFollowRedirects() { this.steps.add(new DoNotFollowRedirectStep()); return this; @@ -80,32 +139,32 @@ public class SamlClientBuilder { /** Creates fresh and issues an AuthnRequest to the SAML endpoint */ public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) { - return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this)); + return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this)); } /** Issues the given AuthnRequest to the SAML endpoint */ public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) { - return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this)); + return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this)); } /** Issues the given AuthnRequest to the SAML endpoint */ public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) { - return addStep(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this)); + return addStepBuilder(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this)); } /** Handles login page */ public LoginBuilder login() { - return addStep(new LoginBuilder(this)); + return addStepBuilder(new LoginBuilder(this)); } /** Starts IdP-initiated flow for the given client */ public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) { - return addStep(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this)); + return addStepBuilder(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this)); } /** Handles "Requires consent" page */ public RequiredConsentBuilder consentRequired() { - return addStep(new RequiredConsentBuilder(this)); + return addStepBuilder(new RequiredConsentBuilder(this)); } /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */ @@ -119,20 +178,16 @@ public class SamlClientBuilder { public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) { return doNotFollowRedirects() - .addStep(new ModifySamlResponseStepBuilder(responseBinding, this)); + .addStepBuilder(new ModifySamlResponseStepBuilder(responseBinding, this)); } public SamlClientBuilder navigateTo(String httpGetUri) { - steps.add((client, currentURI, currentResponse, context) -> { - return new HttpGet(httpGetUri); - }); + steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri)); return this; } public SamlClientBuilder navigateTo(URI httpGetUri) { - steps.add((client, currentURI, currentResponse, context) -> { - return new HttpGet(httpGetUri); - }); + steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri)); return this; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java index ce95631d62..f71b757eac 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java @@ -37,7 +37,6 @@ import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -import java.util.function.Consumer; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.math.NumberUtils; import org.jboss.arquillian.container.test.api.*; @@ -49,13 +48,21 @@ import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.util.Matchers; +import org.keycloak.testsuite.util.SamlClient; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.net.MalformedURLException; +import java.util.function.BiConsumer; +import org.apache.http.client.methods.HttpGet; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.ui.WebDriverWait; -import static org.hamcrest.Matchers.*; -import static org.keycloak.testsuite.AbstractAuthTest.createUserRepresentation; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.admin.Users.setPasswordFor; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAnnotation; import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; @@ -130,15 +137,21 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda public void startServer() throws Exception { prepareServerDirectory("standalone-" + NODE_1_NAME); controller.start(NODE_1_SERVER_NAME); - prepareWorkerNode(Integer.valueOf(System.getProperty("app.server.1.management.port"))); + prepareWorkerNode(0, Integer.valueOf(System.getProperty("app.server.1.management.port"))); prepareServerDirectory("standalone-" + NODE_2_NAME); controller.start(NODE_2_SERVER_NAME); - prepareWorkerNode(Integer.valueOf(System.getProperty("app.server.2.management.port"))); + prepareWorkerNode(1, Integer.valueOf(System.getProperty("app.server.2.management.port"))); deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME); deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME + "_2"); } - protected abstract void prepareWorkerNode(Integer managementPort) throws Exception; + /** + * Prepares a worker node + * @param nodeIndex Node index, counting from 0 + * @param managementPort Port for management operations on this node + * @throws Exception + */ + protected abstract void prepareWorkerNode(int nodeIndex, Integer managementPort) throws Exception; @After public void stopServer() { @@ -155,41 +168,103 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda loginActionsPage.setAuthRealm(DEMO); } - protected void testLogoutViaSessionIndex(URL employeeUrl, Consumer logoutFunction) { - EmployeeServletDistributable page = PageFactory.initElements(driver, EmployeeServletDistributable.class); - page.setUrl(employeeUrl); - page.getUriBuilder().port(HTTP_PORT_NODE_REVPROXY); - - UserRepresentation bburkeUser = createUserRepresentation("bburke", "bburke@redhat.com", "Bill", "Burke", true); + protected void testLogoutViaSessionIndex(URL employeeUrl, boolean forceRefreshAtOtherNode, BiConsumer logoutFunction) { setPasswordFor(bburkeUser, CredentialRepresentation.PASSWORD); - assertSuccessfulLogin(page, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke"); + final String employeeUrlString; + try { + URL employeeUrlAtRevProxy = new URL(employeeUrl.getProtocol(), employeeUrl.getHost(), HTTP_PORT_NODE_REVPROXY, employeeUrl.getFile()); + employeeUrlString = employeeUrlAtRevProxy.toString(); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } - updateProxy(NODE_2_NAME, NODE_2_URI, NODE_1_URI); - logoutFunction.accept(page); - delayedCheckLoggedOut(page, loginActionsPage); + SamlClientBuilder builder = new SamlClientBuilder() + // Go to employee URL at reverse proxy which is set to forward to first node + .navigateTo(employeeUrlString) + // process redirection to login page + .processSamlResponse(Binding.POST).build() + .login().user(bburkeUser).build() + .processSamlResponse(Binding.POST).build() + + // Returned to the page + .assertResponse(Matchers.bodyHC(containsString("principal=bburke"))) + + // Update the proxy to forward to the second node. + .addStep(() -> updateProxy(NODE_2_NAME, NODE_2_URI, NODE_1_URI)); + + if (forceRefreshAtOtherNode) { + // Go to employee URL at reverse proxy which is set to forward to _second_ node now + builder + .navigateTo(employeeUrlString) + .doNotFollowRedirects() + .assertResponse(Matchers.bodyHC(containsString("principal=bburke"))); + } + + // Logout at the _second_ node + logoutFunction.accept(builder, employeeUrlString); + + SamlClient samlClient = builder.execute(); + delayedCheckLoggedOut(samlClient, employeeUrlString); + + // Update the proxy to forward to the first node. updateProxy(NODE_1_NAME, NODE_1_URI, NODE_2_URI); - delayedCheckLoggedOut(page, loginActionsPage); + delayedCheckLoggedOut(samlClient, employeeUrlString); + } + + private void delayedCheckLoggedOut(SamlClient samlClient, String url) { + Retry.execute(() -> { + samlClient.execute( + (client, currentURI, currentResponse, context) -> new HttpGet(url), + (client, currentURI, currentResponse, context) -> { + assertThat(currentResponse, Matchers.bodyHC(not(containsString("principal=bburke")))); + return null; + } + ); + }, 10, 300); + } + + private void logoutViaAdminConsole() { + RealmResource demoRealm = adminClient.realm(DEMO); + String bburkeId = ApiUtil.findUserByUsername(demoRealm, "bburke").getId(); + demoRealm.users().get(bburkeId).logout(); + log.infov("Logged out via admin console"); } @Test - public void testBackchannelLogout(@ArquillianResource + public void testAdminInitiatedBackchannelLogout(@ArquillianResource @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception { - testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> { - RealmResource demoRealm = adminClient.realm(DEMO); - String bburkeId = ApiUtil.findUserByUsername(demoRealm, "bburke").getId(); - demoRealm.users().get(bburkeId).logout(); - log.infov("Logged out via admin console"); + testLogoutViaSessionIndex(employeeUrl, false, (builder, url) -> builder.addStep(this::logoutViaAdminConsole)); + } + + @Test + public void testAdminInitiatedBackchannelLogoutWithAssertionOfLoggedIn(@ArquillianResource + @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception { + testLogoutViaSessionIndex(employeeUrl, true, (builder, url) -> builder.addStep(this::logoutViaAdminConsole)); + } + + @Test + public void testUserInitiatedFrontchannelLogout(@ArquillianResource + @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception { + testLogoutViaSessionIndex(employeeUrl, false, (builder, url) -> { + builder + .navigateTo(url + "?GLO=true") + .processSamlResponse(Binding.POST).build() // logout request + .processSamlResponse(Binding.POST).build() // logout response + ; }); } @Test - public void testFrontchannelLogout(@ArquillianResource + public void testUserInitiatedFrontchannelLogoutWithAssertionOfLoggedIn(@ArquillianResource @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception { - testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> { - page.logout(); - log.infov("Logged out via application"); + testLogoutViaSessionIndex(employeeUrl, true, (builder, url) -> { + builder + .navigateTo(url + "?GLO=true") + .processSamlResponse(Binding.POST).build() // logout request + .processSamlResponse(Binding.POST).build() // logout response + ; }); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml index 14bd44ef05..16798d0139 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml @@ -23,7 +23,7 @@ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" logoutPage="/logout.jsp" forceAuthentication="false"> - + diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl index 114d875d92..24fe3e2800 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl @@ -13,25 +13,29 @@ - - demo - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB - /auth - EXTERNAL - customer-portal-subsystem - password - - - - demo - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB - /auth - EXTERNAL - product-portal-subsystem - password - + + + + demo + MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB + /auth + EXTERNAL + customer-portal-subsystem + password + + + demo + MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB + /auth + EXTERNAL + product-portal-subsystem + password + + + + diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java index f0a166bea4..b52a8debf9 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java @@ -54,8 +54,8 @@ public class EAP6SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest { } @Override - protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException { - log.infov("Preparing worker node ({0})", managementPort); + protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort); OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions .standalone() diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/crossdc/EAP6SAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/crossdc/EAP6SAMLAdapterCrossDCTest.java new file mode 100644 index 0000000000..3a726ce20b --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/crossdc/EAP6SAMLAdapterCrossDCTest.java @@ -0,0 +1,169 @@ +/* + * 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.testsuite.adapter.crossdc; + +import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable; +import org.keycloak.testsuite.arquillian.annotation.*; + +import java.io.*; + +import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest; +import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet; + +import org.apache.commons.lang3.math.NumberUtils; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.dmr.ModelNode; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.wildfly.extras.creaper.core.*; +import org.wildfly.extras.creaper.core.online.*; +import org.wildfly.extras.creaper.core.online.operations.*; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment; + +/** + * + * @author hmlnarik + */ +@Ignore("Infinispan version 5 does not support remote cache events, hence this test is left here for development purposes only") +@AppServerContainer("app-server-eap6") +public class EAP6SAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusterTest { + + @BeforeClass + public static void checkCrossDcTest() { + Assume.assumeThat("Seems not to be running cross-DC tests", System.getProperty("cache.server"), not(is("undefined"))); + } + + protected static final int PORT_OFFSET_CACHE_1 = NumberUtils.toInt(System.getProperty("cache.server.port.offset"), 0); + protected static final int CACHE_HOTROD_PORT_CACHE_1 = 11222 + PORT_OFFSET_CACHE_1; + protected static final int PORT_OFFSET_CACHE_2 = NumberUtils.toInt(System.getProperty("cache.server.2.port.offset"), 0); + protected static final int CACHE_HOTROD_PORT_CACHE_2 = 11222 + PORT_OFFSET_CACHE_2; + + private final int[] CACHE_HOTROD_PORTS = new int[] { CACHE_HOTROD_PORT_CACHE_1, CACHE_HOTROD_PORT_CACHE_2 }; + private final int[] TCPPING_PORTS = new int[] { 7600 + PORT_OFFSET_NODE_1, 7600 + PORT_OFFSET_NODE_2 }; + + private static final String SESSION_CACHE_NAME = EmployeeServletDistributable.DEPLOYMENT_NAME + "-cache"; + private static final String SSO_CACHE_NAME = SESSION_CACHE_NAME + ".ssoCache"; + + private static final Address SESSION_CACHE_ADDR = Address.subsystem("infinispan") + .and("cache-container", "web") + .and("replicated-cache", SESSION_CACHE_NAME); + private static final Address SSO_CACHE_ADDR = Address.subsystem("infinispan") + .and("cache-container", "web") + .and("replicated-cache", SSO_CACHE_NAME); + + private static final String JBOSS_WEB_XML = "\n" + + "\n" + + " \n" + + " SESSION\n" + + " " + "web." + SESSION_CACHE_NAME + "\n" + + " \n" + + ""; + + @TargetsContainer(value = "app-server-eap6-" + NODE_1_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false) + protected static WebArchive employee() { + return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, + EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", + SendUsernameServlet.class) + .addAsWebInfResource(new StringAsset(JBOSS_WEB_XML), "jboss-web.xml"); + } + + @TargetsContainer(value = "app-server-eap6-" + NODE_2_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false) + protected static WebArchive employee2() { + return employee(); + } + + @Override + protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort); + + OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions + .standalone() + .hostAndPort("localhost", managementPort) + .protocol(ManagementProtocol.REMOTE) + .build()); + Operations op = new Operations(clientWorkerNodeClient); + + Batch b = new Batch(); + Address tcppingStack = Address + .subsystem("jgroups") + .and("stack", "tcpping"); + b.add(tcppingStack); + b.add(tcppingStack.and("transport", "TRANSPORT"), Values.of("socket-binding", "jgroups-tcp").and("type", "TCP")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "TCPPING")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + TCPPING_PORTS[nodeIndex] + "]")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "1")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "MERGE2")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "FD_SOCK").and("socket-binding", "jgroups-tcp-fd")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "FD")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "VERIFY_SUSPECT")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.NAKACK")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "UNICAST2")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.STABLE")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.GMS")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "UFC")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "MFC")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "FRAG2")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "RSVP")); + Assert.assertTrue("Could not add TCPPING JGroups stack", op.batch(b).isSuccess()); + + op.add(Address.of("socket-binding-group", "standard-sockets").and("remote-destination-outbound-socket-binding", "cache-server"), + Values.of("host", "localhost") + .and("port", CACHE_HOTROD_PORTS[nodeIndex])); + + op.add(SESSION_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC")); + op.add(SESSION_CACHE_ADDR.and("remote-store", "REMOTE_STORE"), + Values.of("remote-servers", ModelNode.fromString("[{\"outbound-socket-binding\"=>\"cache-server\"}]")) + .and("cache", SESSION_CACHE_NAME) + .and("passivation", false) + .and("purge", false) + .and("preload", false) + .and("shared", true) + ); + + op.add(SSO_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC")); + op.add(SSO_CACHE_ADDR.and("remote-store", "REMOTE_STORE"), + Values.of("remote-servers", ModelNode.fromString("[{\"outbound-socket-binding\"=>\"cache-server\"}]")) + .and("cache", SSO_CACHE_NAME) + .and("passivation", false) + .and("purge", false) + .and("preload", false) + .and("shared", true) + ); + + Assert.assertTrue(op.writeAttribute(Address.subsystem("jgroups"), "default-stack", "tcpping").isSuccess()); + Assert.assertTrue(op.writeAttribute(Address.subsystem("web"), "instance-id", "${jboss.node.name}").isSuccess()); + op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem")); + op.add(Address.subsystem("keycloak-saml")); + + clientWorkerNodeClient.execute("reload"); + + log.infov("Worker node ({0}) Prepared", managementPort); + } + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml index 061e94e179..2fe7f5e1ba 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml @@ -36,13 +36,19 @@ org.wildfly.extras.creaper creaper-core test - 1.5.0 + 1.6.1 org.wildfly.core wildfly-cli test - 3.0.0.Beta30 + ${wildfly.core.version} + + + org.wildfly.core + wildfly-controller-client + test + ${wildfly.core.version} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java index eb7973c37d..5735a6aed4 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java @@ -53,8 +53,8 @@ public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTes } @Override - protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException { - log.infov("Preparing worker node ({0})", managementPort); + protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort); OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions .standalone() @@ -71,8 +71,6 @@ public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTes b.add(tcppingStack.and("protocol", "TCPPING")); b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + (7600 + PORT_OFFSET_NODE_1) + "],localhost[" + (7600 + PORT_OFFSET_NODE_2) + "]")); b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); - b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "2")); - b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000")); b.add(tcppingStack.and("protocol", "MERGE3")); b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd")); b.add(tcppingStack.and("protocol", "FD")); diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/crossdc/WildflySAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/crossdc/WildflySAMLAdapterCrossDCTest.java new file mode 100644 index 0000000000..9288f20a9c --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/crossdc/WildflySAMLAdapterCrossDCTest.java @@ -0,0 +1,162 @@ +/* + * 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.testsuite.adapter.crossdc; + +import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable; +import org.keycloak.testsuite.arquillian.annotation.*; + +import java.io.*; + +import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest; +import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet; + +import org.apache.commons.lang3.math.NumberUtils; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.wildfly.extras.creaper.core.*; +import org.wildfly.extras.creaper.core.online.*; +import org.wildfly.extras.creaper.core.online.operations.*; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment; + +/** + * + * @author hmlnarik + */ +@AppServerContainer("app-server-wildfly") +public class WildflySAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusterTest { + + @BeforeClass + public static void checkCrossDcTest() { + Assume.assumeThat("Seems not to be running cross-DC tests", System.getProperty("cache.server"), not(is("undefined"))); + } + + protected static final int PORT_OFFSET_CACHE_1 = NumberUtils.toInt(System.getProperty("cache.server.port.offset"), 0); + protected static final int CACHE_HOTROD_PORT_CACHE_1 = 11222 + PORT_OFFSET_CACHE_1; + protected static final int PORT_OFFSET_CACHE_2 = NumberUtils.toInt(System.getProperty("cache.server.2.port.offset"), 0); + protected static final int CACHE_HOTROD_PORT_CACHE_2 = 11222 + PORT_OFFSET_CACHE_2; + + private final int[] CACHE_HOTROD_PORTS = new int[] { CACHE_HOTROD_PORT_CACHE_1, CACHE_HOTROD_PORT_CACHE_2 }; + private final int[] TCPPING_PORTS = new int[] { 7600 + PORT_OFFSET_NODE_1, 7600 + PORT_OFFSET_NODE_2 }; + + private static final String SESSION_CACHE_NAME = EmployeeServletDistributable.DEPLOYMENT_NAME + "-cache"; + private static final String SSO_CACHE_NAME = SESSION_CACHE_NAME + ".ssoCache"; + + private static final Address SESSION_CACHE_ADDR = Address.subsystem("infinispan") + .and("cache-container", "web") + .and("replicated-cache", SESSION_CACHE_NAME); + private static final Address SSO_CACHE_ADDR = Address.subsystem("infinispan") + .and("cache-container", "web") + .and("replicated-cache", SSO_CACHE_NAME); + + private static final String JBOSS_WEB_XML = "\n" + + "\n" + + " \n" + + " SESSION\n" + + " " + "web." + SESSION_CACHE_NAME + "\n" + + " \n" + + ""; + + @TargetsContainer(value = "app-server-wildfly-" + NODE_1_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false) + protected static WebArchive employee() { + return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, + EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", + SendUsernameServlet.class) + .addAsWebInfResource(new StringAsset(JBOSS_WEB_XML), "jboss-web.xml"); + } + + @TargetsContainer(value = "app-server-wildfly-" + NODE_2_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false) + protected static WebArchive employee2() { + return employee(); + } + + @Override + protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort); + + OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions + .standalone() + .hostAndPort("localhost", managementPort) + .build()); + Operations op = new Operations(clientWorkerNodeClient); + + Batch b = new Batch(); + Address tcppingStack = Address + .subsystem("jgroups") + .and("stack", "tcpping"); + b.add(tcppingStack); + b.add(tcppingStack.and("transport", "TCP"), Values.of("socket-binding", "jgroups-tcp")); + b.add(tcppingStack.and("protocol", "TCPPING")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + TCPPING_PORTS[nodeIndex] + "]")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); + b.add(tcppingStack.and("protocol", "MERGE3")); + b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd")); + b.add(tcppingStack.and("protocol", "FD")); + b.add(tcppingStack.and("protocol", "VERIFY_SUSPECT")); + b.add(tcppingStack.and("protocol", "pbcast.NAKACK2")); + b.add(tcppingStack.and("protocol", "UNICAST3")); + b.add(tcppingStack.and("protocol", "pbcast.STABLE")); + b.add(tcppingStack.and("protocol", "pbcast.GMS")); + b.add(tcppingStack.and("protocol", "MFC")); + b.add(tcppingStack.and("protocol", "FRAG2")); + b.writeAttribute(Address.subsystem("jgroups").and("channel", "ee"), "stack", "tcpping"); + op.batch(b); + + + op.add(Address.of("socket-binding-group", "standard-sockets").and("remote-destination-outbound-socket-binding", "cache-server"), + Values.of("host", "localhost") + .and("port", CACHE_HOTROD_PORTS[nodeIndex])); + + op.add(SESSION_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC")); + op.writeAttribute(SESSION_CACHE_ADDR.and("component", "locking"), "isolation", "REPEATABLE_READ"); + op.writeAttribute(SESSION_CACHE_ADDR.and("component", "transaction"), "mode", "BATCH"); + op.add(SESSION_CACHE_ADDR.and("store", "remote"), + Values.ofList("remote-servers", "cache-server") + .and("cache", SESSION_CACHE_NAME) + .and("passivation", false) + .and("purge", false) + .and("preload", false) + .and("shared", true) + ); + + op.add(SSO_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC")); + op.add(SSO_CACHE_ADDR.and("store", "remote"), + Values.ofList("remote-servers", "cache-server") + .and("cache", SSO_CACHE_NAME) + .and("passivation", false) + .and("purge", false) + .and("preload", false) + .and("shared", true) + ); + + op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem")); + op.add(Address.subsystem("keycloak-saml")); + + clientWorkerNodeClient.execute("reload"); + + log.infov("Worker node ({0}) Prepared", managementPort); + } + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java index 5a2644846e..d80fda5aaa 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java @@ -53,8 +53,8 @@ public class Wildfly10SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterT } @Override - protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException { - log.infov("Preparing worker node ({0})", managementPort); + protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort); OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions .standalone() @@ -71,8 +71,6 @@ public class Wildfly10SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterT b.add(tcppingStack.and("protocol", "TCPPING")); b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + (7600 + PORT_OFFSET_NODE_1) + "],localhost[" + (7600 + PORT_OFFSET_NODE_2) + "]")); b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); - b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "2")); - b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000")); b.add(tcppingStack.and("protocol", "MERGE3")); b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd")); b.add(tcppingStack.and("protocol", "FD")); diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/crossdc/Wildfly10SAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/crossdc/Wildfly10SAMLAdapterCrossDCTest.java new file mode 100644 index 0000000000..9c7d935db6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/crossdc/Wildfly10SAMLAdapterCrossDCTest.java @@ -0,0 +1,162 @@ +/* + * 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.testsuite.adapter.crossdc; + +import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable; +import org.keycloak.testsuite.arquillian.annotation.*; + +import java.io.*; + +import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest; +import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet; + +import org.apache.commons.lang3.math.NumberUtils; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.wildfly.extras.creaper.core.*; +import org.wildfly.extras.creaper.core.online.*; +import org.wildfly.extras.creaper.core.online.operations.*; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment; + +/** + * + * @author hmlnarik + */ +@AppServerContainer("app-server-wildfly10") +public class Wildfly10SAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusterTest { + + @BeforeClass + public static void checkCrossDcTest() { + Assume.assumeThat("Seems not to be running cross-DC tests", System.getProperty("cache.server"), not(is("undefined"))); + } + + protected static final int PORT_OFFSET_CACHE_1 = NumberUtils.toInt(System.getProperty("cache.server.port.offset"), 0); + protected static final int CACHE_HOTROD_PORT_CACHE_1 = 11222 + PORT_OFFSET_CACHE_1; + protected static final int PORT_OFFSET_CACHE_2 = NumberUtils.toInt(System.getProperty("cache.server.2.port.offset"), 0); + protected static final int CACHE_HOTROD_PORT_CACHE_2 = 11222 + PORT_OFFSET_CACHE_2; + + private final int[] CACHE_HOTROD_PORTS = new int[] { CACHE_HOTROD_PORT_CACHE_1, CACHE_HOTROD_PORT_CACHE_2 }; + private final int[] TCPPING_PORTS = new int[] { 7600 + PORT_OFFSET_NODE_1, 7600 + PORT_OFFSET_NODE_2 }; + + private static final String SESSION_CACHE_NAME = EmployeeServletDistributable.DEPLOYMENT_NAME + "-cache"; + private static final String SSO_CACHE_NAME = SESSION_CACHE_NAME + ".ssoCache"; + + private static final Address SESSION_CACHE_ADDR = Address.subsystem("infinispan") + .and("cache-container", "web") + .and("replicated-cache", SESSION_CACHE_NAME); + private static final Address SSO_CACHE_ADDR = Address.subsystem("infinispan") + .and("cache-container", "web") + .and("replicated-cache", SSO_CACHE_NAME); + + private static final String JBOSS_WEB_XML = "\n" + + "\n" + + " \n" + + " SESSION\n" + + " " + "web." + SESSION_CACHE_NAME + "\n" + + " \n" + + ""; + + @TargetsContainer(value = "app-server-wildfly10-" + NODE_1_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false) + protected static WebArchive employee() { + return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, + EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", + SendUsernameServlet.class) + .addAsWebInfResource(new StringAsset(JBOSS_WEB_XML), "jboss-web.xml"); + } + + @TargetsContainer(value = "app-server-wildfly10-" + NODE_2_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false) + protected static WebArchive employee2() { + return employee(); + } + + @Override + protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort); + + OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions + .standalone() + .hostAndPort("localhost", managementPort) + .build()); + Operations op = new Operations(clientWorkerNodeClient); + + Batch b = new Batch(); + Address tcppingStack = Address + .subsystem("jgroups") + .and("stack", "tcpping"); + b.add(tcppingStack); + b.add(tcppingStack.and("transport", "TCP"), Values.of("socket-binding", "jgroups-tcp")); + b.add(tcppingStack.and("protocol", "TCPPING")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + TCPPING_PORTS[nodeIndex] + "]")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); + b.add(tcppingStack.and("protocol", "MERGE3")); + b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd")); + b.add(tcppingStack.and("protocol", "FD")); + b.add(tcppingStack.and("protocol", "VERIFY_SUSPECT")); + b.add(tcppingStack.and("protocol", "pbcast.NAKACK2")); + b.add(tcppingStack.and("protocol", "UNICAST3")); + b.add(tcppingStack.and("protocol", "pbcast.STABLE")); + b.add(tcppingStack.and("protocol", "pbcast.GMS")); + b.add(tcppingStack.and("protocol", "MFC")); + b.add(tcppingStack.and("protocol", "FRAG2")); + b.writeAttribute(Address.subsystem("jgroups").and("channel", "ee"), "stack", "tcpping"); + op.batch(b); + + + op.add(Address.of("socket-binding-group", "standard-sockets").and("remote-destination-outbound-socket-binding", "cache-server"), + Values.of("host", "localhost") + .and("port", CACHE_HOTROD_PORTS[nodeIndex])); + + op.add(SESSION_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC")); + op.writeAttribute(SESSION_CACHE_ADDR.and("component", "locking"), "isolation", "REPEATABLE_READ"); + op.writeAttribute(SESSION_CACHE_ADDR.and("component", "transaction"), "mode", "BATCH"); + op.add(SESSION_CACHE_ADDR.and("store", "remote"), + Values.ofList("remote-servers", "cache-server") + .and("cache", SESSION_CACHE_NAME) + .and("passivation", false) + .and("purge", false) + .and("preload", false) + .and("shared", true) + ); + + op.add(SSO_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC")); + op.add(SSO_CACHE_ADDR.and("store", "remote"), + Values.ofList("remote-servers", "cache-server") + .and("cache", SSO_CACHE_NAME) + .and("passivation", false) + .and("purge", false) + .and("preload", false) + .and("shared", true) + ); + + op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem")); + op.add(Address.subsystem("keycloak-saml")); + + clientWorkerNodeClient.execute("reload"); + + log.infov("Worker node ({0}) Prepared", managementPort); + } + +}