KEYCLOAK-4995 Support for distributed SAML logout in cross DC
This commit is contained in:
parent
3f8083e34c
commit
794c508b10
28 changed files with 1324 additions and 189 deletions
|
@ -78,6 +78,18 @@
|
||||||
<version>7.1.2.Final</version>
|
<version>7.1.2.Final</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.infinispan</groupId>
|
||||||
|
<artifactId>infinispan-core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>5.2.20.Final</version> <!-- override version to match EAP's -->
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.infinispan</groupId>
|
||||||
|
<artifactId>infinispan-cachestore-remote</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>5.2.20.Final</version> <!-- override version to match EAP's -->
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-saml-tomcat-adapter-core</artifactId>
|
<artifactId>keycloak-saml-tomcat-adapter-core</artifactId>
|
||||||
|
|
|
@ -16,9 +16,11 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.saml.jbossweb.infinispan;
|
package org.keycloak.adapters.saml.jbossweb.infinispan;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.AdapterConstants;
|
||||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
import org.keycloak.adapters.spi.SessionIdMapperUpdater;
|
import org.keycloak.adapters.spi.SessionIdMapperUpdater;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import javax.naming.InitialContext;
|
import javax.naming.InitialContext;
|
||||||
import javax.naming.NamingException;
|
import javax.naming.NamingException;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
|
@ -26,6 +28,8 @@ import org.apache.catalina.Context;
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.configuration.cache.CacheMode;
|
import org.infinispan.configuration.cache.CacheMode;
|
||||||
import org.infinispan.configuration.cache.Configuration;
|
import org.infinispan.configuration.cache.Configuration;
|
||||||
|
import org.infinispan.loaders.CacheLoaderManager;
|
||||||
|
import org.infinispan.loaders.remote.RemoteCacheStore;
|
||||||
import org.infinispan.manager.EmbeddedCacheManager;
|
import org.infinispan.manager.EmbeddedCacheManager;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
@ -37,24 +41,12 @@ public class InfinispanSessionCacheIdMapperUpdater {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class);
|
private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class);
|
||||||
|
|
||||||
public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container/web";
|
public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container";
|
||||||
|
|
||||||
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 SessionIdMapperUpdater addTokenStoreUpdaters(Context context, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) {
|
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();
|
ServletContext servletContext = context.getServletContext();
|
||||||
String cacheContainerLookup = (servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null)
|
String containerName = servletContext == null ? null : servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
|
||||||
? servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME)
|
String cacheName = servletContext == null ? null : servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME);
|
||||||
: DEFAULT_CACHE_CONTAINER_JNDI_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
|
// 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();
|
String host = context.getParent() == null ? "" : context.getParent().getName();
|
||||||
|
@ -62,43 +54,48 @@ public class InfinispanSessionCacheIdMapperUpdater {
|
||||||
if ("/".equals(contextPath)) {
|
if ("/".equals(contextPath)) {
|
||||||
contextPath = "/ROOT";
|
contextPath = "/ROOT";
|
||||||
}
|
}
|
||||||
|
String deploymentSessionCacheName = host + contextPath;
|
||||||
|
|
||||||
boolean deploymentSessionCacheNamePreset = servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME) != null;
|
if (containerName == null || cacheName == null || deploymentSessionCacheName == null) {
|
||||||
String deploymentSessionCacheName = deploymentSessionCacheNamePreset
|
LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", host + contextPath);
|
||||||
? servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME)
|
|
||||||
: host + contextPath;
|
return previousIdMapperUpdater;
|
||||||
boolean ssoCacheNamePreset = servletContext != null && servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME) != null;
|
}
|
||||||
String ssoCacheName = ssoCacheNamePreset
|
|
||||||
? servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME)
|
String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName;
|
||||||
: deploymentSessionCacheName + ".ssoCache";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
|
EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
|
||||||
|
|
||||||
Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName);
|
Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName);
|
||||||
if (ssoCacheConfiguration == null) {
|
if (ssoCacheConfiguration == null) {
|
||||||
Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
|
Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
|
||||||
if (cacheConfiguration == null) {
|
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();
|
ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
|
||||||
} else {
|
} 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;
|
ssoCacheConfiguration = cacheConfiguration;
|
||||||
cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration);
|
cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration);
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode();
|
||||||
if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) {
|
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<String, String[]> ssoCache = cacheManager.getCache(ssoCacheName, true);
|
Cache<String, String[]> ssoCache = cacheManager.getCache(cacheName, true);
|
||||||
ssoCache.addListener(new SsoSessionCacheListener(mapper));
|
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);
|
SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater);
|
||||||
|
|
||||||
|
@ -108,4 +105,17 @@ public class InfinispanSessionCacheIdMapperUpdater {
|
||||||
return previousIdMapperUpdater;
|
return previousIdMapperUpdater;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void warnIfRemoteStoreIsUsed(Cache<String, String[]> ssoCache) {
|
||||||
|
final List<RemoteCacheStore> 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<RemoteCacheStore> getRemoteStores(Cache ssoCache) {
|
||||||
|
return ssoCache.getAdvancedCache().getComponentRegistry().getComponent(CacheLoaderManager.class).getCacheLoaders(RemoteCacheStore.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.notifications.Listener;
|
import org.infinispan.notifications.Listener;
|
||||||
import org.infinispan.notifications.cachelistener.annotation.*;
|
import org.infinispan.notifications.cachelistener.annotation.*;
|
||||||
import org.infinispan.notifications.cachelistener.event.*;
|
import org.infinispan.notifications.cachelistener.event.*;
|
||||||
|
@ -43,9 +44,12 @@ public class SsoSessionCacheListener {
|
||||||
|
|
||||||
private final SessionIdMapper idMapper;
|
private final SessionIdMapper idMapper;
|
||||||
|
|
||||||
|
private final Cache<String, String[]> ssoCache;
|
||||||
|
|
||||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
public SsoSessionCacheListener(SessionIdMapper idMapper) {
|
public SsoSessionCacheListener(Cache<String, String[]> ssoCache, SessionIdMapper idMapper) {
|
||||||
|
this.ssoCache = ssoCache;
|
||||||
this.idMapper = idMapper;
|
this.idMapper = idMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +72,10 @@ public class SsoSessionCacheListener {
|
||||||
@CacheEntryRemoved
|
@CacheEntryRemoved
|
||||||
@CacheEntryModified
|
@CacheEntryModified
|
||||||
public void addEvent(TransactionalEvent event) {
|
public void addEvent(TransactionalEvent event) {
|
||||||
if (event.isPre() == false) {
|
if (event.getGlobalTransaction() != null) {
|
||||||
map.get(event.getGlobalTransaction()).add(event);
|
map.get(event.getGlobalTransaction()).add(event);
|
||||||
|
} else {
|
||||||
|
processEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,40 +93,53 @@ public class SsoSessionCacheListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Event e : events) {
|
for (final Event e : events) {
|
||||||
switch (e.getType()) {
|
processEvent(e);
|
||||||
case CACHE_ENTRY_CREATED:
|
}
|
||||||
this.executor.submit(new Runnable() {
|
}
|
||||||
@Override public void run() {
|
|
||||||
cacheEntryCreated((CacheEntryCreatedEvent) e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CACHE_ENTRY_MODIFIED:
|
private void processEvent(final Event e) {
|
||||||
this.executor.submit(new Runnable() {
|
switch (e.getType()) {
|
||||||
@Override public void run() {
|
case CACHE_ENTRY_CREATED:
|
||||||
cacheEntryModified((CacheEntryModifiedEvent) e);
|
this.executor.submit(new Runnable() {
|
||||||
}
|
@Override public void run() {
|
||||||
});
|
cacheEntryCreated((CacheEntryCreatedEvent) e);
|
||||||
break;
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CACHE_ENTRY_MODIFIED:
|
||||||
|
this.executor.submit(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
cacheEntryModified((CacheEntryModifiedEvent) e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
case CACHE_ENTRY_REMOVED:
|
case CACHE_ENTRY_REMOVED:
|
||||||
this.executor.submit(new Runnable() {
|
this.executor.submit(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
cacheEntryRemoved((CacheEntryRemovedEvent) e);
|
cacheEntryRemoved((CacheEntryRemovedEvent) e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cacheEntryCreated(CacheEntryCreatedEvent event) {
|
private void cacheEntryCreated(CacheEntryCreatedEvent event) {
|
||||||
if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) {
|
if (! (event.getKey() instanceof String)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String httpSessionId = (String) event.getKey();
|
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 ssoId = value[0];
|
||||||
String principal = value[1];
|
String principal = value[1];
|
||||||
|
|
||||||
|
|
|
@ -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 <replicationConfig>
|
||||||
|
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<ParamValueMetaData> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -48,6 +48,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
|
||||||
Phase.POST_MODULE, // PHASE
|
Phase.POST_MODULE, // PHASE
|
||||||
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
|
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
|
||||||
chooseConfigDeploymentProcessor());
|
chooseConfigDeploymentProcessor());
|
||||||
|
processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME,
|
||||||
|
Phase.POST_MODULE, // PHASE
|
||||||
|
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
|
||||||
|
chooseClusteredSsoDeploymentProcessor());
|
||||||
}
|
}
|
||||||
}, OperationContext.Stage.RUNTIME);
|
}, OperationContext.Stage.RUNTIME);
|
||||||
}
|
}
|
||||||
|
@ -60,6 +64,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
|
||||||
return new KeycloakAdapterConfigDeploymentProcessor();
|
return new KeycloakAdapterConfigDeploymentProcessor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DeploymentUnitProcessor chooseClusteredSsoDeploymentProcessor() {
|
||||||
|
return new KeycloakClusteredSsoDeploymentProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
|
protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,6 @@ package org.keycloak.adapters.saml;
|
||||||
*/
|
*/
|
||||||
public class AdapterConstants {
|
public class AdapterConstants {
|
||||||
public static final String AUTH_DATA_PARAM_NAME="org.keycloak.saml.xml.adapterConfig";
|
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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,12 +152,19 @@ public class ServletSamlSessionStore implements SamlSessionStore {
|
||||||
public boolean isLoggedIn() {
|
public boolean isLoggedIn() {
|
||||||
HttpSession session = getSession(false);
|
HttpSession session = getSession(false);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
log.debug("session was null, returning null");
|
log.debug("Session was not found");
|
||||||
return false;
|
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());
|
final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
|
||||||
if (samlSession == null) {
|
if (samlSession == null) {
|
||||||
log.debug("SamlSession was not in session, returning null");
|
log.debug("SamlSession was not found in the session");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,10 @@
|
||||||
<groupId>org.infinispan</groupId>
|
<groupId>org.infinispan</groupId>
|
||||||
<artifactId>infinispan-core</artifactId>
|
<artifactId>infinispan-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.infinispan</groupId>
|
||||||
|
<artifactId>infinispan-cachestore-remote</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.picketbox</groupId>
|
<groupId>org.picketbox</groupId>
|
||||||
<artifactId>picketbox</artifactId>
|
<artifactId>picketbox</artifactId>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.saml.wildfly.infinispan;
|
package org.keycloak.adapters.saml.wildfly.infinispan;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.AdapterConstants;
|
||||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
import org.keycloak.adapters.spi.SessionIdMapperUpdater;
|
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.CacheMode;
|
||||||
import org.infinispan.configuration.cache.Configuration;
|
import org.infinispan.configuration.cache.Configuration;
|
||||||
import org.infinispan.manager.EmbeddedCacheManager;
|
import org.infinispan.manager.EmbeddedCacheManager;
|
||||||
|
import org.infinispan.persistence.manager.PersistenceManager;
|
||||||
|
import org.infinispan.persistence.remote.RemoteStore;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,64 +40,55 @@ public class InfinispanSessionCacheIdMapperUpdater {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class);
|
private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class);
|
||||||
|
|
||||||
public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container/web";
|
public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container";
|
||||||
|
|
||||||
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 SessionIdMapperUpdater addTokenStoreUpdaters(DeploymentInfo deploymentInfo, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) {
|
public static SessionIdMapperUpdater addTokenStoreUpdaters(DeploymentInfo deploymentInfo, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) {
|
||||||
boolean distributable = Objects.equals(
|
Map<String, String> initParameters = deploymentInfo.getInitParameters();
|
||||||
deploymentInfo.getSessionManagerFactory().getClass().getName(),
|
String containerName = initParameters == null ? null : initParameters.get(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
|
||||||
"org.wildfly.clustering.web.undertow.session.DistributableSessionManagerFactory"
|
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;
|
return previousIdMapperUpdater;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> initParameters = deploymentInfo.getInitParameters();
|
String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName;
|
||||||
String cacheContainerLookup = (initParameters != null && initParameters.get(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null)
|
String deploymentSessionCacheName = deploymentInfo.getDeploymentName();
|
||||||
? 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";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
|
EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
|
||||||
|
|
||||||
Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName);
|
Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName);
|
||||||
if (ssoCacheConfiguration == null) {
|
if (ssoCacheConfiguration == null) {
|
||||||
Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
|
Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
|
||||||
if (cacheConfiguration == null) {
|
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();
|
ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
|
||||||
} else {
|
} 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;
|
ssoCacheConfiguration = cacheConfiguration;
|
||||||
cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration);
|
cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration);
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode();
|
||||||
if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) {
|
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<String, String[]> ssoCache = cacheManager.getCache(ssoCacheName, true);
|
Cache<String, String[]> ssoCache = cacheManager.getCache(cacheName, true);
|
||||||
ssoCache.addListener(new SsoSessionCacheListener(mapper));
|
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);
|
SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater);
|
||||||
deploymentInfo.addSessionListener(updater);
|
deploymentInfo.addSessionListener(updater);
|
||||||
|
|
||||||
|
@ -104,4 +98,25 @@ public class InfinispanSessionCacheIdMapperUpdater {
|
||||||
return previousIdMapperUpdater;
|
return previousIdMapperUpdater;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addSsoCacheCrossDcListener(Cache<String, String[]> ssoCache, SsoSessionCacheListener listener) {
|
||||||
|
if (ssoCache.getCacheConfiguration().persistence() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<RemoteStore> 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<RemoteStore> getRemoteStores(Cache ispnCache) {
|
||||||
|
return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,12 @@ import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
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.Listener;
|
||||||
import org.infinispan.notifications.cachelistener.annotation.*;
|
import org.infinispan.notifications.cachelistener.annotation.*;
|
||||||
import org.infinispan.notifications.cachelistener.event.*;
|
import org.infinispan.notifications.cachelistener.event.*;
|
||||||
|
@ -34,6 +40,7 @@ import org.jboss.logging.Logger;
|
||||||
* @author hmlnarik
|
* @author hmlnarik
|
||||||
*/
|
*/
|
||||||
@Listener
|
@Listener
|
||||||
|
@ClientListener
|
||||||
public class SsoSessionCacheListener {
|
public class SsoSessionCacheListener {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class);
|
private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class);
|
||||||
|
@ -42,14 +49,21 @@ public class SsoSessionCacheListener {
|
||||||
|
|
||||||
private final SessionIdMapper idMapper;
|
private final SessionIdMapper idMapper;
|
||||||
|
|
||||||
|
private final Cache<String, String[]> ssoCache;
|
||||||
|
|
||||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
public SsoSessionCacheListener(SessionIdMapper idMapper) {
|
public SsoSessionCacheListener(Cache<String, String[]> ssoCache, SessionIdMapper idMapper) {
|
||||||
|
this.ssoCache = ssoCache;
|
||||||
this.idMapper = idMapper;
|
this.idMapper = idMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TransactionRegistered
|
@TransactionRegistered
|
||||||
public void startTransaction(TransactionRegisteredEvent event) {
|
public void startTransaction(TransactionRegisteredEvent event) {
|
||||||
|
if (event.getGlobalTransaction() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue<Event>());
|
map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue<Event>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,42 +80,56 @@ public class SsoSessionCacheListener {
|
||||||
@CacheEntryCreated
|
@CacheEntryCreated
|
||||||
@CacheEntryRemoved
|
@CacheEntryRemoved
|
||||||
public void addEvent(TransactionalEvent event) {
|
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);
|
map.get(event.getGlobalTransaction().globalId()).add(event);
|
||||||
|
} else {
|
||||||
|
processEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TransactionCompleted
|
@TransactionCompleted
|
||||||
public void endTransaction(TransactionCompletedEvent event) {
|
public void endTransaction(TransactionCompletedEvent event) {
|
||||||
|
if (event.getGlobalTransaction() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Queue<Event> events = map.remove(event.getGlobalTransaction().globalId());
|
Queue<Event> events = map.remove(event.getGlobalTransaction().globalId());
|
||||||
|
|
||||||
if (events == null || ! event.isTransactionSuccessful()) {
|
if (events == null || ! event.isTransactionSuccessful()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.isOriginLocal()) {
|
|
||||||
// Local events are processed by local HTTP session listener
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Event e : events) {
|
for (final Event e : events) {
|
||||||
switch (e.getType()) {
|
processEvent(e);
|
||||||
case CACHE_ENTRY_CREATED:
|
}
|
||||||
this.executor.submit(new Runnable() {
|
}
|
||||||
@Override public void run() {
|
|
||||||
cacheEntryCreated((CacheEntryCreatedEvent) e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CACHE_ENTRY_REMOVED:
|
private void processEvent(final Event e) {
|
||||||
this.executor.submit(new Runnable() {
|
switch (e.getType()) {
|
||||||
@Override public void run() {
|
case CACHE_ENTRY_CREATED:
|
||||||
cacheEntryRemoved((CacheEntryRemovedEvent) e);
|
this.executor.submit(new Runnable() {
|
||||||
}
|
@Override public void run() {
|
||||||
});
|
cacheEntryCreated((CacheEntryCreatedEvent) e);
|
||||||
break;
|
}
|
||||||
}
|
});
|
||||||
|
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());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <replicationConfig>
|
||||||
|
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<ParamValueMetaData> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -43,6 +43,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
|
||||||
Phase.POST_MODULE, // PHASE
|
Phase.POST_MODULE, // PHASE
|
||||||
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
|
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
|
||||||
chooseConfigDeploymentProcessor());
|
chooseConfigDeploymentProcessor());
|
||||||
|
processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME,
|
||||||
|
Phase.POST_MODULE, // PHASE
|
||||||
|
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
|
||||||
|
chooseClusteredSsoDeploymentProcessor());
|
||||||
}
|
}
|
||||||
}, OperationContext.Stage.RUNTIME);
|
}, OperationContext.Stage.RUNTIME);
|
||||||
}
|
}
|
||||||
|
@ -54,4 +58,8 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
|
||||||
private DeploymentUnitProcessor chooseConfigDeploymentProcessor() {
|
private DeploymentUnitProcessor chooseConfigDeploymentProcessor() {
|
||||||
return new KeycloakAdapterConfigDeploymentProcessor();
|
return new KeycloakAdapterConfigDeploymentProcessor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DeploymentUnitProcessor chooseClusteredSsoDeploymentProcessor() {
|
||||||
|
return new KeycloakClusteredSsoDeploymentProcessor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,11 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
|
||||||
ssoToSession.put(sso, session);
|
ssoToSession.put(sso, session);
|
||||||
sessionToSso.put(session, sso);
|
sessionToSso.put(session, sso);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (principal == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> userSessions = principalToSession.get(principal);
|
Set<String> userSessions = principalToSession.get(principal);
|
||||||
if (userSessions == null) {
|
if (userSessions == null) {
|
||||||
final Set<String> tmp = Collections.synchronizedSet(new HashSet<String>());
|
final Set<String> tmp = Collections.synchronizedSet(new HashSet<String>());
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
<module name="org.jboss.as.security"/>
|
<module name="org.jboss.as.security"/>
|
||||||
<module name="org.jboss.as.web"/>
|
<module name="org.jboss.as.web"/>
|
||||||
<module name="org.picketbox"/>
|
<module name="org.picketbox"/>
|
||||||
<module name="org.keycloak.keycloak-saml-as7-adapter"/>
|
|
||||||
<module name="org.keycloak.keycloak-adapter-spi"/>
|
<module name="org.keycloak.keycloak-adapter-spi"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core-public"/>
|
<module name="org.keycloak.keycloak-saml-core-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core"/>
|
<module name="org.keycloak.keycloak-saml-core"/>
|
||||||
|
|
|
@ -40,5 +40,6 @@
|
||||||
<module name="org.jboss.as.web-common"/>
|
<module name="org.jboss.as.web-common"/>
|
||||||
<module name="org.jboss.metadata"/>
|
<module name="org.jboss.metadata"/>
|
||||||
<module name="org.apache.httpcomponents"/>
|
<module name="org.apache.httpcomponents"/>
|
||||||
|
<module name="org.infinispan.cachestore.remote"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -39,6 +39,8 @@
|
||||||
<local-cache name="loginFailures" configuration="sessions-cfg" />
|
<local-cache name="loginFailures" configuration="sessions-cfg" />
|
||||||
<local-cache name="actionTokens" configuration="sessions-cfg" />
|
<local-cache name="actionTokens" configuration="sessions-cfg" />
|
||||||
<local-cache name="work" configuration="sessions-cfg" />
|
<local-cache name="work" configuration="sessions-cfg" />
|
||||||
|
<local-cache name="employee-distributable-cache.ssoCache" configuration="sessions-cfg"/>
|
||||||
|
<local-cache name="employee-distributable-cache" configuration="sessions-cfg"/>
|
||||||
</xsl:copy>
|
</xsl:copy>
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
|
|
||||||
|
@ -57,6 +59,8 @@
|
||||||
<replicated-cache name="loginFailures" configuration="sessions-cfg" />
|
<replicated-cache name="loginFailures" configuration="sessions-cfg" />
|
||||||
<replicated-cache name="actionTokens" configuration="sessions-cfg" />
|
<replicated-cache name="actionTokens" configuration="sessions-cfg" />
|
||||||
<replicated-cache name="work" configuration="sessions-cfg" />
|
<replicated-cache name="work" configuration="sessions-cfg" />
|
||||||
|
<replicated-cache name="employee-distributable-cache.ssoCache" configuration="sessions-cfg"/>
|
||||||
|
<replicated-cache name="employee-distributable-cache" configuration="sessions-cfg"/>
|
||||||
</xsl:copy>
|
</xsl:copy>
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
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<Step> steps) {
|
||||||
|
executeAndTransform(resp -> null, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T executeAndTransform(ResultExtractor<T> resultTransformer, Step... steps) {
|
||||||
|
return executeAndTransform(resultTransformer, Arrays.asList(steps));
|
||||||
|
}
|
||||||
|
|
||||||
public <T> T executeAndTransform(ResultExtractor<T> resultTransformer, List<Step> steps) {
|
public <T> T executeAndTransform(ResultExtractor<T> resultTransformer, List<Step> steps) {
|
||||||
CloseableHttpResponse currentResponse = null;
|
CloseableHttpResponse currentResponse = null;
|
||||||
URI currentUri = URI.create("about:blank");
|
URI currentUri = URI.create("about:blank");
|
||||||
|
|
|
@ -33,6 +33,10 @@ import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.LoginBuilder;
|
import org.keycloak.testsuite.util.saml.LoginBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
|
import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
|
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;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +47,19 @@ public class SamlClientBuilder {
|
||||||
|
|
||||||
private final List<Step> steps = new LinkedList<>();
|
private final List<Step> 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<CloseableHttpResponse> resultConsumer) {
|
public SamlClient execute(Consumer<CloseableHttpResponse> resultConsumer) {
|
||||||
final SamlClient samlClient = new SamlClient();
|
final SamlClient samlClient = new SamlClient();
|
||||||
samlClient.executeAndTransform(r -> {
|
samlClient.executeAndTransform(r -> {
|
||||||
|
@ -52,6 +69,11 @@ public class SamlClientBuilder {
|
||||||
return samlClient;
|
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> T executeAndTransform(ResultExtractor<T> resultTransformer) {
|
public <T> T executeAndTransform(ResultExtractor<T> resultTransformer) {
|
||||||
return new SamlClient().executeAndTransform(resultTransformer, steps);
|
return new SamlClient().executeAndTransform(resultTransformer, steps);
|
||||||
}
|
}
|
||||||
|
@ -60,11 +82,48 @@ public class SamlClientBuilder {
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T extends Step> T addStep(T step) {
|
public <T extends Step> T addStepBuilder(T step) {
|
||||||
steps.add(step);
|
steps.add(step);
|
||||||
return 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<HttpResponse> 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() {
|
public SamlClientBuilder doNotFollowRedirects() {
|
||||||
this.steps.add(new DoNotFollowRedirectStep());
|
this.steps.add(new DoNotFollowRedirectStep());
|
||||||
return this;
|
return this;
|
||||||
|
@ -80,32 +139,32 @@ public class SamlClientBuilder {
|
||||||
|
|
||||||
/** Creates fresh and issues an AuthnRequest to the SAML endpoint */
|
/** Creates fresh and issues an AuthnRequest to the SAML endpoint */
|
||||||
public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) {
|
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 */
|
/** Issues the given AuthnRequest to the SAML endpoint */
|
||||||
public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) {
|
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 */
|
/** Issues the given AuthnRequest to the SAML endpoint */
|
||||||
public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) {
|
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 */
|
/** Handles login page */
|
||||||
public LoginBuilder login() {
|
public LoginBuilder login() {
|
||||||
return addStep(new LoginBuilder(this));
|
return addStepBuilder(new LoginBuilder(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Starts IdP-initiated flow for the given client */
|
/** Starts IdP-initiated flow for the given client */
|
||||||
public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) {
|
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 */
|
/** Handles "Requires consent" page */
|
||||||
public RequiredConsentBuilder consentRequired() {
|
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. */
|
/** 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) {
|
public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) {
|
||||||
return
|
return
|
||||||
doNotFollowRedirects()
|
doNotFollowRedirects()
|
||||||
.addStep(new ModifySamlResponseStepBuilder(responseBinding, this));
|
.addStepBuilder(new ModifySamlResponseStepBuilder(responseBinding, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SamlClientBuilder navigateTo(String httpGetUri) {
|
public SamlClientBuilder navigateTo(String httpGetUri) {
|
||||||
steps.add((client, currentURI, currentResponse, context) -> {
|
steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri));
|
||||||
return new HttpGet(httpGetUri);
|
|
||||||
});
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SamlClientBuilder navigateTo(URI httpGetUri) {
|
public SamlClientBuilder navigateTo(URI httpGetUri) {
|
||||||
steps.add((client, currentURI, currentResponse, context) -> {
|
steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri));
|
||||||
return new HttpGet(httpGetUri);
|
|
||||||
});
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ import java.net.URL;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.lang3.math.NumberUtils;
|
import org.apache.commons.lang3.math.NumberUtils;
|
||||||
import org.jboss.arquillian.container.test.api.*;
|
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.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
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.TimeoutException;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
import org.openqa.selenium.support.PageFactory;
|
|
||||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.keycloak.testsuite.AbstractAuthTest.createUserRepresentation;
|
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.admin.Users.setPasswordFor;
|
||||||
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAnnotation;
|
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAnnotation;
|
||||||
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
|
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
|
||||||
|
@ -130,15 +137,21 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda
|
||||||
public void startServer() throws Exception {
|
public void startServer() throws Exception {
|
||||||
prepareServerDirectory("standalone-" + NODE_1_NAME);
|
prepareServerDirectory("standalone-" + NODE_1_NAME);
|
||||||
controller.start(NODE_1_SERVER_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);
|
prepareServerDirectory("standalone-" + NODE_2_NAME);
|
||||||
controller.start(NODE_2_SERVER_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);
|
||||||
deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME + "_2");
|
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
|
@After
|
||||||
public void stopServer() {
|
public void stopServer() {
|
||||||
|
@ -155,41 +168,103 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda
|
||||||
loginActionsPage.setAuthRealm(DEMO);
|
loginActionsPage.setAuthRealm(DEMO);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void testLogoutViaSessionIndex(URL employeeUrl, Consumer<EmployeeServletDistributable> logoutFunction) {
|
protected void testLogoutViaSessionIndex(URL employeeUrl, boolean forceRefreshAtOtherNode, BiConsumer<SamlClientBuilder, String> 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);
|
|
||||||
setPasswordFor(bburkeUser, CredentialRepresentation.PASSWORD);
|
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);
|
SamlClientBuilder builder = new SamlClientBuilder()
|
||||||
logoutFunction.accept(page);
|
// Go to employee URL at reverse proxy which is set to forward to first node
|
||||||
delayedCheckLoggedOut(page, loginActionsPage);
|
.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);
|
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
|
@Test
|
||||||
public void testBackchannelLogout(@ArquillianResource
|
public void testAdminInitiatedBackchannelLogout(@ArquillianResource
|
||||||
@OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
|
@OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
|
||||||
testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> {
|
testLogoutViaSessionIndex(employeeUrl, false, (builder, url) -> builder.addStep(this::logoutViaAdminConsole));
|
||||||
RealmResource demoRealm = adminClient.realm(DEMO);
|
}
|
||||||
String bburkeId = ApiUtil.findUserByUsername(demoRealm, "bburke").getId();
|
|
||||||
demoRealm.users().get(bburkeId).logout();
|
@Test
|
||||||
log.infov("Logged out via admin console");
|
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
|
@Test
|
||||||
public void testFrontchannelLogout(@ArquillianResource
|
public void testUserInitiatedFrontchannelLogoutWithAssertionOfLoggedIn(@ArquillianResource
|
||||||
@OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
|
@OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
|
||||||
testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> {
|
testLogoutViaSessionIndex(employeeUrl, true, (builder, url) -> {
|
||||||
page.logout();
|
builder
|
||||||
log.infov("Logged out via application");
|
.navigateTo(url + "?GLO=true")
|
||||||
|
.processSamlResponse(Binding.POST).build() // logout request
|
||||||
|
.processSamlResponse(Binding.POST).build() // logout response
|
||||||
|
;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||||
logoutPage="/logout.jsp"
|
logoutPage="/logout.jsp"
|
||||||
forceAuthentication="false">
|
forceAuthentication="false">
|
||||||
<PrincipalNameMapping policy="FROM_NAME_ID"/>
|
<PrincipalNameMapping policy="FROM_ATTRIBUTE" attribute="email"/>
|
||||||
<RoleIdentifiers>
|
<RoleIdentifiers>
|
||||||
<Attribute name="memberOf"/>
|
<Attribute name="memberOf"/>
|
||||||
<Attribute name="Role"/>
|
<Attribute name="Role"/>
|
||||||
|
|
|
@ -13,25 +13,29 @@
|
||||||
<xsl:copy>
|
<xsl:copy>
|
||||||
<xsl:apply-templates select="@* | node()" />
|
<xsl:apply-templates select="@* | node()" />
|
||||||
|
|
||||||
<secure-deployment name="customer-portal-subsystem.war">
|
<xsl:if test="not(*[local-name() = 'secure-deployment'])">
|
||||||
<realm>demo</realm>
|
|
||||||
<realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
|
<secure-deployment name="customer-portal-subsystem.war">
|
||||||
<auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
|
<realm>demo</realm>
|
||||||
<ssl-required>EXTERNAL</ssl-required>
|
<realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
|
||||||
<resource>customer-portal-subsystem</resource>
|
<auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
|
||||||
<credential name="secret">password</credential>
|
<ssl-required>EXTERNAL</ssl-required>
|
||||||
</secure-deployment>
|
<resource>customer-portal-subsystem</resource>
|
||||||
|
<credential name="secret">password</credential>
|
||||||
<secure-deployment name="product-portal-subsystem.war">
|
</secure-deployment>
|
||||||
<realm>demo</realm>
|
|
||||||
<realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
|
|
||||||
<auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
|
|
||||||
<ssl-required>EXTERNAL</ssl-required>
|
|
||||||
<resource>product-portal-subsystem</resource>
|
|
||||||
<credential name="secret">password</credential>
|
|
||||||
</secure-deployment>
|
|
||||||
|
|
||||||
|
<secure-deployment name="product-portal-subsystem.war">
|
||||||
|
<realm>demo</realm>
|
||||||
|
<realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
|
||||||
|
<auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
|
||||||
|
<ssl-required>EXTERNAL</ssl-required>
|
||||||
|
<resource>product-portal-subsystem</resource>
|
||||||
|
<credential name="secret">password</credential>
|
||||||
|
</secure-deployment>
|
||||||
|
|
||||||
|
</xsl:if>
|
||||||
</xsl:copy>
|
</xsl:copy>
|
||||||
|
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
|
|
||||||
<xsl:template match="@*|node()">
|
<xsl:template match="@*|node()">
|
||||||
|
|
|
@ -54,8 +54,8 @@ public class EAP6SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
|
protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
|
||||||
log.infov("Preparing worker node ({0})", managementPort);
|
log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort);
|
||||||
|
|
||||||
OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
|
OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
|
||||||
.standalone()
|
.standalone()
|
||||||
|
|
|
@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
+ "<jboss-web>\n"
|
||||||
|
+ " <replication-config>\n"
|
||||||
|
+ " <replication-granularity>SESSION</replication-granularity>\n"
|
||||||
|
+ " <cache-name>" + "web." + SESSION_CACHE_NAME + "</cache-name>\n"
|
||||||
|
+ " </replication-config>\n"
|
||||||
|
+ "</jboss-web>";
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -36,13 +36,19 @@
|
||||||
<groupId>org.wildfly.extras.creaper</groupId>
|
<groupId>org.wildfly.extras.creaper</groupId>
|
||||||
<artifactId>creaper-core</artifactId>
|
<artifactId>creaper-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
<version>1.5.0</version>
|
<version>1.6.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.wildfly.core</groupId>
|
<groupId>org.wildfly.core</groupId>
|
||||||
<artifactId>wildfly-cli</artifactId>
|
<artifactId>wildfly-cli</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
<version>3.0.0.Beta30</version>
|
<version>${wildfly.core.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.wildfly.core</groupId>
|
||||||
|
<artifactId>wildfly-controller-client</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
<version>${wildfly.core.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
|
@ -53,8 +53,8 @@ public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTes
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
|
protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
|
||||||
log.infov("Preparing worker node ({0})", managementPort);
|
log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort);
|
||||||
|
|
||||||
OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
|
OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
|
||||||
.standalone()
|
.standalone()
|
||||||
|
@ -71,8 +71,6 @@ public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTes
|
||||||
b.add(tcppingStack.and("protocol", "TCPPING"));
|
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", "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", "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", "MERGE3"));
|
||||||
b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd"));
|
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", "FD"));
|
||||||
|
|
|
@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
+ "<jboss-web>\n"
|
||||||
|
+ " <replication-config>\n"
|
||||||
|
+ " <replication-granularity>SESSION</replication-granularity>\n"
|
||||||
|
+ " <cache-name>" + "web." + SESSION_CACHE_NAME + "</cache-name>\n"
|
||||||
|
+ " </replication-config>\n"
|
||||||
|
+ "</jboss-web>";
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -53,8 +53,8 @@ public class Wildfly10SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterT
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
|
protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
|
||||||
log.infov("Preparing worker node ({0})", managementPort);
|
log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort);
|
||||||
|
|
||||||
OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
|
OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
|
||||||
.standalone()
|
.standalone()
|
||||||
|
@ -71,8 +71,6 @@ public class Wildfly10SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterT
|
||||||
b.add(tcppingStack.and("protocol", "TCPPING"));
|
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", "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", "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", "MERGE3"));
|
||||||
b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd"));
|
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", "FD"));
|
||||||
|
|
|
@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
+ "<jboss-web>\n"
|
||||||
|
+ " <replication-config>\n"
|
||||||
|
+ " <replication-granularity>SESSION</replication-granularity>\n"
|
||||||
|
+ " <cache-name>" + "web." + SESSION_CACHE_NAME + "</cache-name>\n"
|
||||||
|
+ " </replication-config>\n"
|
||||||
|
+ "</jboss-web>";
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue