diff --git a/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java b/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java index 1fdfff8d6a..78fc4c7f98 100644 --- a/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java +++ b/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java @@ -17,6 +17,8 @@ package org.keycloak.common.util; +import java.util.Collections; +import java.util.Map; import java.util.Properties; /** @@ -24,9 +26,21 @@ import java.util.Properties; */ public class SystemEnvProperties extends Properties { + private final Map overrides; + + public SystemEnvProperties(Map overrides) { + this.overrides = overrides; + } + + public SystemEnvProperties() { + this.overrides = Collections.EMPTY_MAP; + } + @Override public String getProperty(String key) { - if (key.startsWith("env.")) { + if (overrides.containsKey(key)) { + return overrides.get(key); + } else if (key.startsWith("env.")) { return System.getenv().get(key.substring(4)); } else { return System.getProperty(key); diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md index 4146eaa9e6..3313f3fe4b 100644 --- a/misc/CrossDataCenter.md +++ b/misc/CrossDataCenter.md @@ -3,6 +3,8 @@ Test Cross-Data-Center scenario (test with external JDG server) These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented. +Note that these steps are already automated, see Cross-DC tests section in [HOW-TO-RUN.md](../testsuite/integration-arquillian/HOW-TO-RUN.md) document. + What is working right now is: - Propagating of invalidation messages for "realms" and "users" caches - All the other things provided by ClusterProvider, which is: @@ -18,7 +20,7 @@ Basic setup This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point to external JDG server. - + JDG Server setup ---------------- - Download JDG 7.0 server and unzip to some folder diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 17ae1219fb..76b0779367 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -154,7 +154,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon if (clustered) { String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); - configureTransport(gcb, nodeName); + String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR)); + configureTransport(gcb, nodeName, jgroupsUdpMcastAddr); } gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains); @@ -317,24 +318,40 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon return cb.build(); } - protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName) { + private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object(); + + protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String jgroupsUdpMcastAddr) { if (nodeName == null) { gcb.transport().defaultTransport(); } else { FileLookup fileLookup = FileLookupFactory.newInstance(); - try { - // Compatibility with Wildfly - JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader())); - channel.setName(nodeName); - JGroupsTransport transport = new JGroupsTransport(channel); + synchronized (CHANNEL_INIT_SYNCHRONIZER) { + String originalMcastAddr = System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR); + if (jgroupsUdpMcastAddr == null) { + System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR); + } else { + System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, jgroupsUdpMcastAddr); + } + try { + // Compatibility with Wildfly + JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader())); + channel.setName(nodeName); + JGroupsTransport transport = new JGroupsTransport(channel); - gcb.transport().nodeName(nodeName); - gcb.transport().transport(transport); + gcb.transport().nodeName(nodeName); + gcb.transport().transport(transport); - logger.infof("Configured jgroups transport with the channel name: %s", nodeName); - } catch (Exception e) { - throw new RuntimeException(e); + logger.infof("Configured jgroups transport with the channel name: %s", nodeName); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + if (originalMcastAddr == null) { + System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR); + } else { + System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, originalMcastAddr); + } + } } } } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index 8618a699ec..7fd26521b3 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -53,6 +53,7 @@ public interface InfinispanConnectionProvider extends Provider { // System property used on Wildfly to identify distributedCache address and sticky session route String JBOSS_NODE_NAME = "jboss.node.name"; + String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr"; Cache getCache(String name); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java index c9832ff558..4480f7af9a 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java @@ -220,7 +220,7 @@ public abstract class CacheManager { addInvalidationsFromEvent(event, invalidations); - getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event); + getLogger().debugf("[%s] Invalidating %d cache items after received event %s", cache.getCacheManager().getAddress(), invalidations.size(), event); for (String invalidation : invalidations) { invalidateObject(invalidation); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AddInvalidatedActionTokenEvent.java similarity index 96% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AddInvalidatedActionTokenEvent.java index 37a1a218d5..2bced7e451 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AddInvalidatedActionTokenEvent.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models.cache.infinispan; +package org.keycloak.models.cache.infinispan.events; import org.keycloak.cluster.ClusterEvent; import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RemoveActionTokensSpecificEvent.java similarity index 95% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RemoveActionTokensSpecificEvent.java index 0a4d858b52..d658f30c58 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RemoveActionTokensSpecificEvent.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models.cache.infinispan; +package org.keycloak.models.cache.infinispan.events; import org.keycloak.cluster.ClusterEvent; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java index 127879a4d1..f02fb5a403 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java @@ -19,8 +19,8 @@ package org.keycloak.models.sessions.infinispan; import org.keycloak.cluster.ClusterProvider; import org.keycloak.models.*; -import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent; -import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent; +import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent; +import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; import java.util.*; @@ -58,7 +58,12 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue), false); + AddInvalidatedActionTokenEvent event = new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue); + this.tx.notify(cluster, generateActionTokenEventId(), event, false); + } + + private static String generateActionTokenEventId() { + return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID(); } @Override @@ -93,6 +98,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi } ClusterProvider cluster = session.getProvider(ClusterProvider.class); - this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false); + this.tx.notify(cluster, generateActionTokenEventId(), new RemoveActionTokensSpecificEvent(userId, actionId), false); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java index a8c5e3899e..f67f28f6c0 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java @@ -23,14 +23,16 @@ import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.*; -import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent; -import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent; +import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent; +import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; import java.util.Objects; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; import org.infinispan.context.Flag; +import org.infinispan.remoting.transport.Address; +import org.jboss.logging.Logger; /** * @@ -38,6 +40,10 @@ import org.infinispan.context.Flag; */ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenStoreProviderFactory { + private static final Logger LOG = Logger.getLogger(InfinispanActionTokenStoreProviderFactory.class); + + private volatile Cache actionTokenCache; + public static final String ACTION_TOKEN_EVENTS = "ACTION_TOKEN_EVENTS"; /** @@ -49,34 +55,7 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto @Override public ActionTokenStoreProvider create(KeycloakSession session) { - InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); - Cache actionTokenCache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE); - - ClusterProvider cluster = session.getProvider(ClusterProvider.class); - - cluster.registerListener(ACTION_TOKEN_EVENTS, event -> { - if (event instanceof RemoveActionTokensSpecificEvent) { - RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event; - - actionTokenCache - .getAdvancedCache() - .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD) - .keySet() - .stream() - .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId())) - .forEach(actionTokenCache::remove); - } else if (event instanceof AddInvalidatedActionTokenEvent) { - AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event; - - if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) { - actionTokenCache.put(e.getKey(), e.getTokenValue()); - } else { - actionTokenCache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS); - } - } - }); - - return new InfinispanActionTokenStoreProvider(session, actionTokenCache); + return new InfinispanActionTokenStoreProvider(session, this.actionTokenCache); } @Override @@ -84,8 +63,57 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto this.config = config; } + private static Cache initActionTokenCache(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE); + final Address cacheAddress = cache.getCacheManager().getAddress(); + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + + cluster.registerListener(ClusterProvider.ALL, event -> { + if (event instanceof RemoveActionTokensSpecificEvent) { + RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event; + + LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId()); + + cache + .getAdvancedCache() + .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD) + .keySet() + .stream() + .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId())) + .forEach(cache::remove); + } else if (event instanceof AddInvalidatedActionTokenEvent) { + AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event; + + LOG.debugf("[%s] Invalidating token %s", cacheAddress, e.getKey()); + if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) { + cache.put(e.getKey(), e.getTokenValue()); + } else { + cache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS); + } + } + }); + + LOG.debugf("[%s] Registered cluster listeners", cacheAddress); + + return cache; + } + @Override public void postInit(KeycloakSessionFactory factory) { + Cache cache = this.actionTokenCache; + + // It is necessary to put the cache initialization here, otherwise the cache would be initialized lazily, that + // means also listeners will start only after first cache initialization - that would be too late + if (cache == null) { + synchronized (this) { + cache = this.actionTokenCache; + if (cache == null) { + this.actionTokenCache = initActionTokenCache(factory.create()); + } + } + } } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java index 83e970ddaa..a9589ccca5 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java @@ -92,7 +92,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic ClusterProvider cluster = session.getProvider(ClusterProvider.class); cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes); - log.debug("Registered cluster listeners"); + log.debugf("[%s] Registered cluster listeners", authSessionsCache.getCacheManager().getAddress()); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java index 173c43497d..24742e58d6 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java @@ -81,6 +81,11 @@ public class ActionTokenReducedKey implements Serializable { && Objects.equals(this.actionVerificationNonce, other.getActionVerificationNonce()); } + @Override + public String toString() { + return "userId=" + userId + ", actionId=" + actionId + ", actionVerificationNonce=" + actionVerificationNonce; + } + public static class ExternalizerImpl implements Externalizer { @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java index 7c0f663da6..7e3c76ed5e 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java @@ -53,7 +53,7 @@ public class ActionTokenValueEntity implements ActionTokenValueModel { public void writeObject(ObjectOutput output, ActionTokenValueEntity t) throws IOException { output.writeByte(VERSION_1); - output.writeBoolean(! t.notes.isEmpty()); + output.writeBoolean(t.notes.isEmpty()); if (! t.notes.isEmpty()) { output.writeObject(t.notes); } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 15315d686e..c16bb6a619 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -70,8 +70,10 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; @@ -88,6 +90,8 @@ public class KeycloakApplication extends Application { public static final String KEYCLOAK_EMBEDDED = "keycloak.embedded"; + public static final String SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES = "keycloak.server.context.config.property-overrides"; + private static final Logger logger = Logger.getLogger(KeycloakApplication.class); protected boolean embedded = false; @@ -262,7 +266,7 @@ public class KeycloakApplication extends Application { public static void loadConfig(ServletContext context) { try { JsonNode node = null; - + String dmrConfig = loadDmrConfig(context); if (dmrConfig != null) { node = new ObjectMapper().readTree(dmrConfig); @@ -287,7 +291,13 @@ public class KeycloakApplication extends Application { } if (node != null) { - Properties properties = new SystemEnvProperties(); + Map propertyOverridesMap = new HashMap<>(); + String propertyOverrides = context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES); + if (context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES) != null) { + JsonNode jsonObj = new ObjectMapper().readTree(propertyOverrides); + jsonObj.fields().forEachRemaining(e -> propertyOverridesMap.put(e.getKey(), e.getValue().asText())); + } + Properties properties = new SystemEnvProperties(propertyOverridesMap); Config.init(new JsonConfigProvider(node, properties)); } else { throw new RuntimeException("Keycloak config not found."); diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md index 10becee3e5..14ebce5c8d 100644 --- a/testsuite/integration-arquillian/HOW-TO-RUN.md +++ b/testsuite/integration-arquillian/HOW-TO-RUN.md @@ -417,4 +417,22 @@ and argument: `-p 8181` 3) Run loadbalancer (class `SimpleUndertowLoadBalancer`) without arguments and system properties. Loadbalancer runs on port 8180, so you can access Keycloak on `http://localhost:8180/auth` +## Cross-DC tests +Cross-DC tests use 2 data centers, each with one automatically started and one manually controlled backend servers +(currently only Keycloak on Undertow), and 1 frontend loadbalancer server node that sits in front of all servers. +The browser usually communicates directly with the frontent node and the test controls where the HTTP requests +land by adjusting load balancer configuration (e.g. to direct the traffic to only a single DC). + +For an example of a test, see [org.keycloak.testsuite.crossdc.ActionTokenCrossDCTest](tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java). + +The cross DC requires setting a profile specifying used cache server (currently only Infinispan) by specifying +`cache-server-infinispan` profile in maven. + +#### Run Cross-DC Tests from Maven + +Run the following command (adjust the test specification according to your needs): + + `mvn -Pcache-server-infinispan -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test` + +_Someone using IntelliJ IDEA, please describe steps for that IDE_ diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index 0219f402c5..7e36d1a5e7 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -46,6 +46,7 @@ 2.0.1.Final 2.1.0.Alpha3 2.1.0.Alpha2 + 1.2.0.Beta2 2.2.2 1.0.0.Alpha2 @@ -87,6 +88,11 @@ pom import + + org.infinispan.arquillian.container + infinispan-arquillian-impl + ${arquillian-infinispan-container.version} + org.wildfly.arquillian wildfly-arquillian-container-managed diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/arquillian/LoadBalancerController.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/arquillian/LoadBalancerController.java new file mode 100644 index 0000000000..1e435b28d2 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/arquillian/LoadBalancerController.java @@ -0,0 +1,33 @@ +/* + * 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.arquillian; + +/** + * + * @author hmlnarik + */ +public interface LoadBalancerController { + + void enableAllBackendNodes(); + + void disableAllBackendNodes(); + + void enableBackendNodeByName(String nodeName); + + void disableBackendNodeByName(String nodeName); + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml index 4298d35223..fdcb092fdc 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml @@ -70,6 +70,11 @@ org.keycloak keycloak-undertow-adapter + + org.keycloak.testsuite + integration-arquillian-testsuite-providers + ${project.version} + org.keycloak keycloak-servlet-filter-adapter diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java index 87a37d2c2c..b77d9e052d 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java @@ -48,6 +48,8 @@ import org.keycloak.services.filters.KeycloakSessionServletFilter; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.resources.KeycloakApplication; +import org.keycloak.util.JsonSerialization; +import java.io.IOException; import javax.servlet.DispatcherType; import javax.servlet.ServletException; @@ -55,6 +57,7 @@ import java.lang.reflect.Field; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; public class KeycloakOnUndertow implements DeployableContainer { @@ -75,6 +78,14 @@ public class KeycloakOnUndertow implements DeployableContainer keycloakConfigPropertyOverridesMap; private int bindHttpPortOffset = 0; @@ -72,6 +79,18 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat this.remoteMode = remoteMode; } + public String getKeycloakConfigPropertyOverrides() { + return keycloakConfigPropertyOverrides; + } + + public void setKeycloakConfigPropertyOverrides(String keycloakConfigPropertyOverrides) { + this.keycloakConfigPropertyOverrides = keycloakConfigPropertyOverrides; + } + + public Map getKeycloakConfigPropertyOverridesMap() { + return keycloakConfigPropertyOverridesMap; + } + @Override public void validate() throws ConfigurationException { super.validate(); @@ -80,6 +99,15 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat int newPort = basePort + bindHttpPortOffset; setBindHttpPort(newPort); log.info("KeycloakOnUndertow will listen on port: " + newPort); + + if (this.keycloakConfigPropertyOverrides != null) { + try { + TypeReference> typeRef = new TypeReference>() {}; + this.keycloakConfigPropertyOverridesMap = JsonSerialization.sysPropertiesAwareMapper.readValue(this.keycloakConfigPropertyOverrides, typeRef); + } catch (IOException ex) { + throw new ConfigurationException(ex); + } + } // TODO validate workerThreads diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java index 86a7e2ed10..e64b1b2971 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java @@ -17,13 +17,15 @@ package org.keycloak.testsuite.arquillian.undertow; +import java.io.Closeable; + /** * @author Marek Posolda */ -class SetSystemProperty { +class SetSystemProperty implements Closeable { - private String name; - private String oldValue; + private final String name; + private final String oldValue; public SetSystemProperty(String name, String value) { this.name = name; @@ -50,4 +52,9 @@ class SetSystemProperty { } } + @Override + public void close() { + revert(); + } + } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java index 3eda20c8db..3b533f1349 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.arquillian.undertow.lb; import java.net.URI; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -36,6 +35,8 @@ import io.undertow.util.AttachmentKey; import io.undertow.util.Headers; import org.jboss.logging.Logger; import org.keycloak.services.managers.AuthenticationSessionManager; +import java.util.LinkedHashMap; +import java.util.StringTokenizer; /** * Loadbalancer on embedded undertow. Supports sticky session over "AUTH_SESSION_ID" cookie and failover to different node when sticky node not available. @@ -53,8 +54,9 @@ public class SimpleUndertowLoadBalancer { private final String host; private final int port; - private final String nodesString; + private final Map backendNodes; private Undertow undertow; + private LoadBalancingProxyClient lb; public static void main(String[] args) throws Exception { @@ -77,15 +79,14 @@ public class SimpleUndertowLoadBalancer { public SimpleUndertowLoadBalancer(String host, int port, String nodesString) { this.host = host; this.port = port; - this.nodesString = nodesString; - log.infof("Keycloak nodes: %s", nodesString); + this.backendNodes = parseNodes(nodesString); + log.infof("Keycloak nodes: %s", backendNodes); } public void start() { - Map nodes = parseNodes(nodesString); try { - HttpHandler proxyHandler = createHandler(nodes); + HttpHandler proxyHandler = createHandler(); undertow = Undertow.builder() .addHttpListener(port, host) @@ -104,24 +105,51 @@ public class SimpleUndertowLoadBalancer { undertow.stop(); } + public void enableAllBackendNodes() { + backendNodes.forEach((route, uri) -> { + lb.removeHost(uri); + lb.addHost(uri, route); + }); + } - static Map parseNodes(String nodes) { - String[] nodesArray = nodes.split(","); - Map result = new HashMap<>(); + public void disableAllBackendNodes() { + log.debugf("Load balancer: disabling all nodes"); + backendNodes.values().forEach(lb::removeHost); + } - for (String nodeStr : nodesArray) { - String[] node = nodeStr.trim().split("="); - if (node.length != 2) { - throw new IllegalArgumentException("Illegal node format in the configuration: " + nodeStr); - } - result.put(node[0].trim(), node[1].trim()); + public void enableBackendNodeByName(String nodeName) { + URI uri = backendNodes.get(nodeName); + if (uri == null) { + throw new IllegalArgumentException("Invalid node: " + nodeName); + } + log.debugf("Load balancer: enabling node %s", nodeName); + lb.addHost(uri, nodeName); + } + + public void disableBackendNodeByName(String nodeName) { + URI uri = backendNodes.get(nodeName); + if (uri == null) { + throw new IllegalArgumentException("Invalid node: " + nodeName); + } + log.debugf("Load balancer: disabling node %s", nodeName); + lb.removeHost(uri); + } + + static Map parseNodes(String nodes) { + StringTokenizer st = new StringTokenizer(nodes, ","); + Map result = new LinkedHashMap<>(); + + while (st.hasMoreElements()) { + String nodeStr = st.nextToken(); + String[] node = nodeStr.trim().split("=", 2); + result.put(node[0].trim(), URI.create(node[1].trim())); } return result; } - private HttpHandler createHandler(Map backendNodes) throws Exception { + private HttpHandler createHandler() throws Exception { // TODO: configurable options if needed String sessionCookieNames = AuthenticationSessionManager.AUTH_SESSION_ID; @@ -133,15 +161,7 @@ public class SimpleUndertowLoadBalancer { int connectionIdleTimeout = 60; int maxRetryAttempts = backendNodes.size() - 1; - final LoadBalancingProxyClient lb = new CustomLoadBalancingClient(new ExclusivityChecker() { - - @Override - public boolean isExclusivityRequired(HttpServerExchange exchange) { - //we always create a new connection for upgrade requests - return exchange.getRequestHeaders().contains(Headers.UPGRADE); - } - - }, maxRetryAttempts) + lb = new CustomLoadBalancingClient(exchange -> exchange.getRequestHeaders().contains(Headers.UPGRADE), maxRetryAttempts) .setConnectionsPerThread(connectionsPerThread) .setMaxQueueSize(requestQueueSize) .setSoftMaxConnectionsPerThread(cachedConnectionsPerThread) @@ -152,13 +172,10 @@ public class SimpleUndertowLoadBalancer { lb.addSessionCookieName(id); } - for (Map.Entry node : backendNodes.entrySet()) { - String route = node.getKey(); - URI uri = new URI(node.getValue()); - + backendNodes.forEach((route, uri) -> { lb.addHost(uri, route); - log.infof("Added host: %s, route: %s", uri.toString(), route); - } + log.debugf("Added host: %s, route: %s", uri.toString(), route); + }); ProxyHandler handler = new ProxyHandler(lb, maxTime, ResponseCodeHandler.HANDLE_404); return handler; diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java index 3a0312c875..12d3564e00 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java @@ -19,13 +19,17 @@ package org.keycloak.testsuite.arquillian.undertow.lb; import org.arquillian.undertow.UndertowContainerConfiguration; import org.jboss.arquillian.container.spi.ConfigurationException; +import org.jboss.logging.Logger; /** * @author Marek Posolda */ public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerConfiguration { + protected static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerConfiguration.class); + private String nodes = SimpleUndertowLoadBalancer.DEFAULT_NODES; + private int bindHttpPortOffset = 0; public String getNodes() { return nodes; @@ -35,6 +39,14 @@ public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerCo this.nodes = nodes; } + public int getBindHttpPortOffset() { + return bindHttpPortOffset; + } + + public void setBindHttpPortOffset(int bindHttpPortOffset) { + this.bindHttpPortOffset = bindHttpPortOffset; + } + @Override public void validate() throws ConfigurationException { super.validate(); @@ -44,5 +56,11 @@ public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerCo } catch (Exception e) { throw new ConfigurationException(e); } + + int basePort = getBindHttpPort(); + int newPort = basePort + bindHttpPortOffset; + setBindHttpPort(newPort); + log.info("SimpleUndertowLoadBalancer will listen on port: " + newPort); + } } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java index 4b24c15030..a7f1ffed03 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java @@ -25,13 +25,14 @@ import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaD import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.descriptor.api.Descriptor; +import org.keycloak.testsuite.arquillian.LoadBalancerController; /** * Arquillian container over {@link SimpleUndertowLoadBalancer} * * @author Marek Posolda */ -public class SimpleUndertowLoadBalancerContainer implements DeployableContainer { +public class SimpleUndertowLoadBalancerContainer implements DeployableContainer, LoadBalancerController { private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerContainer.class); @@ -84,4 +85,24 @@ public class SimpleUndertowLoadBalancerContainer implements DeployableContainer< public void undeploy(Descriptor descriptor) throws DeploymentException { throw new UnsupportedOperationException("Not implemented"); } + + @Override + public void enableAllBackendNodes() { + this.container.enableAllBackendNodes(); + } + + @Override + public void disableAllBackendNodes() { + this.container.disableAllBackendNodes(); + } + + @Override + public void enableBackendNodeByName(String nodeName) { + this.container.enableBackendNodeByName(nodeName); + } + + @Override + public void disableBackendNodeByName(String nodeName) { + this.container.disableBackendNodeByName(nodeName); + } } diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/assembly.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/assembly.xml new file mode 100644 index 0000000000..d62385322d --- /dev/null +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/assembly.xml @@ -0,0 +1,46 @@ + + + + + ${cache.server.jboss} + + + zip + + + false + + + + ${cache.server.jboss.home} + cache-server-${cache.server} + + **/*.sh + + + + ${cache.server.jboss.home} + cache-server-${cache.server} + + **/*.sh + + 0755 + + + + diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-remote-store.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-remote-store.xsl new file mode 100644 index 0000000000..0d6f8332be --- /dev/null +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-remote-store.xsl @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/io.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/io.xsl new file mode 100644 index 0000000000..03d518a13e --- /dev/null +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/io.xsl @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml new file mode 100644 index 0000000000..3ac23acf1f --- /dev/null +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml @@ -0,0 +1,46 @@ + + + + + + org.keycloak.testsuite + integration-arquillian-servers-cache-server-jboss + 3.2.0.CR1-SNAPSHOT + + 4.0.0 + + integration-arquillian-servers-cache-server-infinispan + pom + Cache Server - JBoss - Infinispan + + + infinispan + cache-server-${cache.server} + ${containers.home}/${cache.server.container} + + org.infinispan.server + infinispan-server + ${infinispan.version} + ${cache.server.jboss.artifactId}-${infinispan.version} + + ${cache.default.worker.io-threads} + ${cache.default.worker.task-max-threads} + + + diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml new file mode 100644 index 0000000000..4bb15556ce --- /dev/null +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml @@ -0,0 +1,210 @@ + + + + + + org.keycloak.testsuite + integration-arquillian-servers-cache-server + 3.2.0.CR1-SNAPSHOT + + 4.0.0 + + integration-arquillian-servers-cache-server-jboss + pom + Cache Server - JBoss Family + + + ${project.parent.basedir}/common + ${project.parent.basedir}/assembly.xml + ${containers.home}/${cache.server.jboss.unpacked.folder.name} + security.xsl + + + + + + cache-server-jboss-submodules + + + src + + + + + + + maven-enforcer-plugin + + + + enforce + + + + + cache.server + cache.server.jboss.groupId + cache.server.jboss.artifactId + cache.server.jboss.version + cache.server.jboss.unpacked.folder.name + + + + + + + + + maven-dependency-plugin + + + unpack-cache-server + generate-resources + + unpack + + + + + ${cache.server.jboss.groupId} + ${cache.server.jboss.artifactId} + ${cache.server.jboss.version} + zip + bin + ${containers.home} + + + + + + + + + org.codehaus.mojo + xml-maven-plugin + + + configure-adapter-debug-log + process-test-resources + + transform + + + + + ${cache.server.jboss.home}/standalone/configuration + + standalone.xml + + ${common.resources}/add-keycloak-remote-store.xsl + ${cache.server.jboss.home}/standalone/configuration + + + + + + io-worker-threads + process-resources + + transform + + + + + ${cache.server.jboss.home}/standalone/configuration + + standalone.xml + standalone-ha.xml + + ${common.resources}/io.xsl + ${cache.server.jboss.home}/standalone/configuration + + + worker.io-threads + ${cache.server.worker.io-threads} + + + worker.task-max-threads + ${cache.server.worker.task-max-threads} + + + + + + + + + + + maven-resources-plugin + + + enable-jboss-mgmt-admin + process-resources + + copy-resources + + + ${cache.server.jboss.home}/standalone/configuration + + + ${common.resources} + + mgmt-users.properties + + + + true + + + + + + + maven-assembly-plugin + + + create-zip + package + + single + + + + ${assembly.xml} + + false + + + + + + + + + + + cache-server-infinispan + + infinispan + + + + + diff --git a/testsuite/integration-arquillian/servers/cache-server/pom.xml b/testsuite/integration-arquillian/servers/cache-server/pom.xml new file mode 100644 index 0000000000..3f5a5a07ab --- /dev/null +++ b/testsuite/integration-arquillian/servers/cache-server/pom.xml @@ -0,0 +1,41 @@ + + + + + + org.keycloak.testsuite + integration-arquillian-servers + 3.2.0.CR1-SNAPSHOT + + 4.0.0 + + integration-arquillian-servers-cache-server + pom + Cache Server + + + ${jboss.default.worker.io-threads} + ${jboss.default.worker.task-max-threads} + + + + jboss + + + diff --git a/testsuite/integration-arquillian/servers/pom.xml b/testsuite/integration-arquillian/servers/pom.xml index 96d6c18db8..e0e1078ae6 100644 --- a/testsuite/integration-arquillian/servers/pom.xml +++ b/testsuite/integration-arquillian/servers/pom.xml @@ -46,13 +46,20 @@ 6.2.1.redhat-084 + + 9.0.1.Final + 16 128 + + 2 + 4 auth-server app-server + cache-server diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 7c52025307..19b38624af 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -40,6 +40,7 @@ - **/cluster/**/*Test.java + **/crossdc/**/*Test.java **/x509/*Test.java @@ -125,6 +126,7 @@ ${exclude.account} ${exclude.client} ${exclude.cluster} + ${exclude.crossdc ${exclude.undertow.adapter} ${exclude.x509} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java index 7e7ee6f92c..94293ddc5d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java @@ -16,7 +16,6 @@ */ package org.keycloak.testsuite.arquillian; -import org.jboss.arquillian.container.spi.Container; import org.jboss.arquillian.container.spi.ContainerRegistry; import org.jboss.arquillian.container.spi.event.StartContainer; import org.jboss.arquillian.container.spi.event.StartSuiteContainers; @@ -41,10 +40,11 @@ import org.keycloak.testsuite.util.OAuthClient; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import javax.ws.rs.NotFoundException; /** @@ -68,8 +68,18 @@ public class AuthServerTestEnricher { private static final String AUTH_SERVER_CONTAINER_PROPERTY = "auth.server.container"; public static final String AUTH_SERVER_CONTAINER = System.getProperty(AUTH_SERVER_CONTAINER_PROPERTY, AUTH_SERVER_CONTAINER_DEFAULT); + private static final String AUTH_SERVER_BACKEND_DEFAULT = AUTH_SERVER_CONTAINER + "-backend"; + private static final String AUTH_SERVER_BACKEND_PROPERTY = "auth.server.backend"; + public static final String AUTH_SERVER_BACKEND = System.getProperty(AUTH_SERVER_BACKEND_PROPERTY, AUTH_SERVER_BACKEND_DEFAULT); + + private static final String AUTH_SERVER_BALANCER_DEFAULT = "auth-server-balancer"; + private static final String AUTH_SERVER_BALANCER_PROPERTY = "auth.server.balancer"; + public static final String AUTH_SERVER_BALANCER = System.getProperty(AUTH_SERVER_BALANCER_PROPERTY, AUTH_SERVER_BALANCER_DEFAULT); + private static final String AUTH_SERVER_CLUSTER_PROPERTY = "auth.server.cluster"; public static final boolean AUTH_SERVER_CLUSTER = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CLUSTER_PROPERTY, "false")); + private static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc"; + public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false")); private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false")); @@ -106,46 +116,82 @@ public class AuthServerTestEnricher { } public void initializeSuiteContext(@Observes(precedence = 2) BeforeSuite event) { - - Set containers = new LinkedHashSet<>(); - for (Container c : containerRegistry.get().getContainers()) { - containers.add(new ContainerInfo(c)); - } + Set containers = containerRegistry.get().getContainers().stream() + .map(ContainerInfo::new) + .collect(Collectors.toSet()); suiteContext = new SuiteContext(containers); - String authServerFrontend = null; + if (AUTH_SERVER_CROSS_DC) { + // if cross-dc mode enabled, load-balancer is the frontend of datacenter cluster + containers.stream() + .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_BALANCER + "-cross-dc")) + .forEach(c -> { + String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0"); + String dcString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("dataCenter", "0"); + updateWithAuthServerInfo(c, Integer.valueOf(portOffsetString)); + suiteContext.addAuthServerInfo(Integer.valueOf(dcString), c); + }); - if (AUTH_SERVER_CLUSTER) { + if (suiteContext.getDcAuthServerInfo().isEmpty()) { + throw new IllegalStateException("Not found frontend container (load balancer): " + AUTH_SERVER_BALANCER); + } + if (suiteContext.getDcAuthServerInfo().stream().anyMatch(Objects::isNull)) { + throw new IllegalStateException("Frontend container (load balancer) misconfiguration"); + } + + containers.stream() + .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_CONTAINER + "-cross-dc-")) + .forEach(c -> { + String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0"); + String dcString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("dataCenter", "0"); + updateWithAuthServerInfo(c, Integer.valueOf(portOffsetString)); + suiteContext.addAuthServerBackendsInfo(Integer.valueOf(dcString), c); + }); + + if (suiteContext.getDcAuthServerInfo().isEmpty()) { + throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", AUTH_SERVER_BACKEND)); + } + if (suiteContext.getDcAuthServerBackendsInfo().stream().anyMatch(Objects::isNull)) { + throw new IllegalStateException("Frontend container (load balancer) misconfiguration"); + } + if (suiteContext.getDcAuthServerBackendsInfo().stream().anyMatch(List::isEmpty)) { + throw new RuntimeException(String.format("Some data center has no auth server container matching '%s' defined in arquillian.xml.", AUTH_SERVER_BACKEND)); + } + + log.info("Using frontend containers: " + this.suiteContext.getDcAuthServerInfo().stream() + .map(ContainerInfo::getQualifier) + .collect(Collectors.joining(", "))); + } else if (AUTH_SERVER_CLUSTER) { // if cluster mode enabled, load-balancer is the frontend - for (ContainerInfo c : containers) { - if (c.getQualifier().startsWith("auth-server-balancer")) { - authServerFrontend = c.getQualifier(); - } + ContainerInfo container = containers.stream() + .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_BALANCER)) + .findAny() + .orElseThrow(() -> new IllegalStateException("Not found frontend container: " + AUTH_SERVER_BALANCER)); + updateWithAuthServerInfo(container); + suiteContext.setAuthServerInfo(container); + + containers.stream() + .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_BACKEND)) + .forEach(c -> { + String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0"); + updateWithAuthServerInfo(c, Integer.valueOf(portOffsetString)); + suiteContext.addAuthServerBackendsInfo(0, c); + }); + + if (suiteContext.getAuthServerBackendsInfo().isEmpty()) { + throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", AUTH_SERVER_BACKEND)); } - if (authServerFrontend != null) { - log.info("Using frontend container: " + authServerFrontend); - } else { - throw new IllegalStateException("Not found frontend container"); - } + log.info("Using frontend container: " + container.getQualifier()); } else { - authServerFrontend = AUTH_SERVER_CONTAINER; // single-node mode - } - - String authServerBackend = AUTH_SERVER_CONTAINER + "-backend"; - int backends = 0; - for (ContainerInfo container : suiteContext.getContainers()) { - // frontend - if (container.getQualifier().equals(authServerFrontend)) { - updateWithAuthServerInfo(container); - suiteContext.setAuthServerInfo(container); - } - // backends - if (AUTH_SERVER_CLUSTER && container.getQualifier().startsWith(authServerBackend)) { - updateWithAuthServerInfo(container, ++backends); - suiteContext.getAuthServerBackendsInfo().add(container); - } + // frontend-only + ContainerInfo container = containers.stream() + .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_CONTAINER)) + .findAny() + .orElseThrow(() -> new IllegalStateException("Not found frontend container: " + AUTH_SERVER_CONTAINER)); + updateWithAuthServerInfo(container); + suiteContext.setAuthServerInfo(container); } // Setup with 2 undertow backend nodes and no loadbalancer. @@ -153,14 +199,6 @@ public class AuthServerTestEnricher { // suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0)); // } - // validate auth server setup - if (suiteContext.getAuthServerInfo() == null) { - throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", authServerFrontend)); - } - if (AUTH_SERVER_CLUSTER && suiteContext.getAuthServerBackendsInfo().isEmpty()) { - throw new RuntimeException(String.format("No auth server container matching '%sN' found in arquillian.xml.", authServerBackend)); - } - if (START_MIGRATION_CONTAINER) { // init migratedAuthServerInfo for (ContainerInfo container : suiteContext.getContainers()) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java index 68c9f17c20..37abe7b29a 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java @@ -5,6 +5,8 @@ import org.jboss.arquillian.container.spi.Container; import java.net.URL; import java.util.Map; import java.util.Objects; +import java.util.stream.Stream; +import org.jboss.arquillian.container.spi.Container.State; /** * @@ -97,4 +99,12 @@ public class ContainerInfo { other.arquillianContainer.getContainerConfiguration().getContainerName()); } + public boolean isStarted() { + return arquillianContainer.getState() == State.STARTED; + } + + public boolean isManual() { + return Objects.equals(arquillianContainer.getContainerConfiguration().getMode(), "manual"); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java index 7b9c13902c..7757b076d1 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java @@ -35,6 +35,7 @@ import org.keycloak.testsuite.arquillian.h2.H2TestEnricher; import org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer; import org.keycloak.testsuite.arquillian.migration.MigrationTestExecutionDecider; import org.keycloak.testsuite.arquillian.provider.AdminClientProvider; +import org.keycloak.testsuite.arquillian.provider.LoadBalancerControllerProvider; import org.keycloak.testsuite.arquillian.provider.OAuthClientProvider; import org.keycloak.testsuite.arquillian.provider.SuiteContextProvider; import org.keycloak.testsuite.arquillian.provider.TestContextProvider; @@ -57,7 +58,8 @@ public class KeycloakArquillianExtension implements LoadableExtension { .service(ResourceProvider.class, SuiteContextProvider.class) .service(ResourceProvider.class, TestContextProvider.class) .service(ResourceProvider.class, AdminClientProvider.class) - .service(ResourceProvider.class, OAuthClientProvider.class); + .service(ResourceProvider.class, OAuthClientProvider.class) + .service(ResourceProvider.class, LoadBalancerControllerProvider.class); builder .service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java index fab303a46d..8a6d300948 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java @@ -24,6 +24,7 @@ import java.util.Set; import org.keycloak.testsuite.arquillian.migration.MigrationContext; +import java.util.LinkedList; import static org.keycloak.testsuite.util.MailServerConfiguration.FROM; import static org.keycloak.testsuite.util.MailServerConfiguration.HOST; import static org.keycloak.testsuite.util.MailServerConfiguration.PORT; @@ -36,8 +37,8 @@ public final class SuiteContext { private final Set container; - private ContainerInfo authServerInfo; - private final List authServerBackendsInfo = new ArrayList<>(); + private List authServerInfo = new LinkedList<>(); + private final List> authServerBackendsInfo = new ArrayList<>(); private ContainerInfo migratedAuthServerInfo; private final MigrationContext migrationContext = new MigrationContext(); @@ -72,17 +73,48 @@ public final class SuiteContext { } public ContainerInfo getAuthServerInfo() { + return getAuthServerInfo(0); + } + + public ContainerInfo getAuthServerInfo(int dcIndex) { + return authServerInfo.get(dcIndex); + } + + public List getDcAuthServerInfo() { return authServerInfo; } public void setAuthServerInfo(ContainerInfo authServerInfo) { - this.authServerInfo = authServerInfo; + this.authServerInfo = new LinkedList<>(); + this.authServerInfo.add(authServerInfo); + } + + public void addAuthServerInfo(int dcIndex, ContainerInfo serverInfo) { + while (dcIndex >= authServerInfo.size()) { + authServerInfo.add(null); + } + this.authServerInfo.set(dcIndex, serverInfo); } public List getAuthServerBackendsInfo() { + return getAuthServerBackendsInfo(0); + } + + public List getAuthServerBackendsInfo(int dcIndex) { + return authServerBackendsInfo.get(dcIndex); + } + + public List> getDcAuthServerBackendsInfo() { return authServerBackendsInfo; } + public void addAuthServerBackendsInfo(int dcIndex, ContainerInfo container) { + while (dcIndex >= authServerBackendsInfo.size()) { + authServerBackendsInfo.add(new LinkedList<>()); + } + authServerBackendsInfo.get(dcIndex).add(container); + } + public ContainerInfo getMigratedAuthServerInfo() { return migratedAuthServerInfo; } @@ -96,7 +128,11 @@ public final class SuiteContext { } public boolean isAuthServerCluster() { - return !authServerBackendsInfo.isEmpty(); + return ! authServerBackendsInfo.isEmpty(); + } + + public boolean isAuthServerCrossDc() { + return authServerBackendsInfo.size() > 1; } public boolean isAuthServerMigrationEnabled() { @@ -113,19 +149,38 @@ public final class SuiteContext { @Override public String toString() { - String containers = "Auth server: " + (isAuthServerCluster() ? "\nFrontend: " : "") - + authServerInfo.getQualifier() + "\n"; - for (ContainerInfo bInfo : getAuthServerBackendsInfo()) { - containers += "Backend: " + bInfo + "\n"; + StringBuilder sb = new StringBuilder("SUITE CONTEXT:\nAuth server: "); + + if (isAuthServerCrossDc()) { + for (int i = 0; i < authServerInfo.size(); i ++) { + ContainerInfo frontend = this.authServerInfo.get(i); + sb.append("\nFrontend (dc=").append(i).append("): ").append(frontend.getQualifier()).append("\n"); + } + + for (int i = 0; i < authServerBackendsInfo.size(); i ++) { + int dcIndex = i; + getDcAuthServerBackendsInfo().get(i).forEach(bInfo -> sb.append("Backend (dc=").append(dcIndex).append("): ").append(bInfo).append("\n")); + } + } else if (isAuthServerCluster()) { + sb.append(isAuthServerCluster() ? "\nFrontend: " : "") + .append(getAuthServerInfo().getQualifier()) + .append("\n"); + + getAuthServerBackendsInfo().forEach(bInfo -> sb.append(" Backend: ").append(bInfo).append("\n")); + } else { + sb.append(getAuthServerInfo().getQualifier()) + .append("\n"); } + + if (isAuthServerMigrationEnabled()) { - containers += "Migrated from: " + System.getProperty("migrated.auth.server.version") + "\n"; + sb.append("Migrated from: ").append(System.getProperty("migrated.auth.server.version")).append("\n"); } + if (isAdapterCompatTesting()) { - containers += "Adapter backward compatibility testing mode!\n"; + sb.append("Adapter backward compatibility testing mode!\n"); } - return "SUITE CONTEXT:\n" - + containers; + return sb.toString(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/LoadBalancer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/LoadBalancer.java new file mode 100644 index 0000000000..a3652a1693 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/LoadBalancer.java @@ -0,0 +1,32 @@ +/* + * 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.arquillian.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author hmlnarik + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface LoadBalancer { + String value() default ""; +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java index 99b6772f1e..a2b6ea735d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java @@ -110,6 +110,8 @@ public class RegistryCreator { if (isClassPresent(getAdapterImplClassValue(containerDef))) { return DeployableContainer.class.isAssignableFrom( loadClass(getAdapterImplClassValue(containerDef))); + } else { + log.warn("Cannot load adapterImpl class for " + containerDef.getContainerName()); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java new file mode 100644 index 0000000000..4f99feb06f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java @@ -0,0 +1,67 @@ +package org.keycloak.testsuite.arquillian.provider; + +import org.keycloak.testsuite.arquillian.annotation.LoadBalancer; +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jboss.arquillian.container.spi.event.KillContainer; +import org.jboss.arquillian.container.spi.event.StartContainer; +import org.jboss.arquillian.container.spi.event.StopContainer; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; +import org.keycloak.testsuite.arquillian.LoadBalancerController; +import org.jboss.arquillian.container.spi.Container; +import org.jboss.arquillian.container.spi.ContainerRegistry; + +/** + * + * @author hmlnarik + */ +public class LoadBalancerControllerProvider implements ResourceProvider { + + @Inject + private Instance registry; + + @Override + public boolean canProvide(Class type) { + return type.equals(LoadBalancerController.class); + } + + @Override + public Object lookup(ArquillianResource resource, Annotation... qualifiers) { + String balancerName = null; + + // Check for the presence of possible qualifiers + for (Annotation a : qualifiers) { + Class annotationType = a.annotationType(); + + if (annotationType.equals(LoadBalancer.class)) { + balancerName = ((LoadBalancer) a).value(); + } + } + + ContainerRegistry reg = registry.get(); + Container container = null; + if (balancerName == null || "".equals(balancerName.trim())) { + if (reg.getContainers().size() == 1) { + container = reg.getContainers().get(0); + } else { + throw new IllegalArgumentException("Invalid load balancer configuration request - need to specify load balancer name in @LoadBalancerController"); + } + } else { + container = reg.getContainer(balancerName); + } + + if (container == null) { + throw new IllegalArgumentException("Invalid load balancer configuration - load balancer not found: '" + balancerName + "'"); + } + if (! (container.getDeployableContainer() instanceof LoadBalancerController)) { + throw new IllegalArgumentException("Invalid load balancer configuration - container " + container.getName() + " is not a load balancer"); + } + + return container.getDeployableContainer(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SetSystemProperty.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SetSystemProperty.java new file mode 100644 index 0000000000..757a385103 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SetSystemProperty.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 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.util; + +import java.io.Closeable; + +/** + * @author Marek Posolda + */ +public class SetSystemProperty implements Closeable { + + private final String name; + private final String oldValue; + + public SetSystemProperty(String name, String value) { + this.name = name; + this.oldValue = System.getProperty(name); + + if (value == null) { + if (oldValue != null) { + System.getProperties().remove(name); + } + } else { + System.setProperty(name, value); + } + } + + public void revert() { + String value = System.getProperty(name); + + if (oldValue == null) { + if (value != null) { + System.getProperties().remove(name); + } + } else { + System.setProperty(name, oldValue); + } + } + + @Override + public void close() { + revert(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java new file mode 100644 index 0000000000..84527f74b1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java @@ -0,0 +1,81 @@ +/* + * 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.crossdc; + +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.events.EventsListenerProviderFactory; +import org.keycloak.testsuite.util.TestCleanup; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; + +/** + * + * @author hmlnarik + */ +public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest { + + protected static final String REALM_NAME = "admin-client-test"; + + protected RealmResource realm; + protected String realmId; + + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + findTestApp(testRealm).setDirectAccessGrantsEnabled(true); + } + + + + @Override + public void addTestRealms(List testRealms) { + super.addTestRealms(testRealms); + + RealmRepresentation adminRealmRep = new RealmRepresentation(); + adminRealmRep.setId(REALM_NAME); + adminRealmRep.setRealm(REALM_NAME); + adminRealmRep.setEnabled(true); + Map config = new HashMap<>(); + config.put("from", "auto@keycloak.org"); + config.put("host", "localhost"); + config.put("port", "3025"); + adminRealmRep.setSmtpServer(config); + + List eventListeners = new ArrayList<>(); + eventListeners.add(JBossLoggingEventListenerProviderFactory.ID); + eventListeners.add(EventsListenerProviderFactory.PROVIDER_ID); + adminRealmRep.setEventsListeners(eventListeners); + + testRealms.add(adminRealmRep); + } + + @Before + public void setRealm() { + realm = adminClient.realm(REALM_NAME); + realmId = realm.toRepresentation().getId(); + } + + @Override + protected TestCleanup getCleanup() { + return getCleanup(REALM_NAME); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java new file mode 100644 index 0000000000..aa674caa41 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java @@ -0,0 +1,161 @@ +/* + * 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.crossdc; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.models.Constants; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.arquillian.ContainerInfo; +import org.keycloak.testsuite.arquillian.LoadBalancerController; +import org.keycloak.testsuite.arquillian.annotation.LoadBalancer; +import org.keycloak.testsuite.auth.page.AuthRealm; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.jboss.arquillian.container.test.api.ContainerController; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.After; +import org.junit.Before; + +/** + * + * @author hmlnarik + */ +public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest { + + @ArquillianResource + @LoadBalancer(value = "auth-server-balancer-cross-dc") + protected LoadBalancerController loadBalancerCtrl; + + @ArquillianResource + protected ContainerController containerController; + + protected Map backendAdminClients = new HashMap<>(); + + @After + @Before + public void enableOnlyFirstNodeInFirstDc() { + this.loadBalancerCtrl.disableAllBackendNodes(); + loadBalancerCtrl.enableBackendNodeByName(getAutomaticallyStartedBackendNodes(0) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No node is started automatically")) + .getQualifier() + ); + } + + @Before + public void terminateManuallyStartedServers() { + log.debug("Halting all nodes that are started manually"); + this.suiteContext.getDcAuthServerBackendsInfo().stream() + .flatMap(List::stream) + .filter(ContainerInfo::isStarted) + .filter(ContainerInfo::isManual) + .map(ContainerInfo::getQualifier) + .forEach(containerController::stop); + } + + @Override + public void importTestRealms() { + enableOnlyFirstNodeInFirstDc(); + super.importTestRealms(); + } + + @Override + public void afterAbstractKeycloakTest() { + enableOnlyFirstNodeInFirstDc(); + super.afterAbstractKeycloakTest(); + } + + @Override + public void deleteCookies() { + enableOnlyFirstNodeInFirstDc(); + super.deleteCookies(); + } + + @Before + public void initLoadBalancer() { + log.debug("Initializing load balancer - only enabling started nodes in the first DC"); + this.loadBalancerCtrl.disableAllBackendNodes(); + // Enable only the started nodes in each datacenter + this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream() + .filter(ContainerInfo::isStarted) + .map(ContainerInfo::getQualifier) + .forEach(loadBalancerCtrl::enableBackendNodeByName); + } + + protected Keycloak createAdminClientFor(ContainerInfo node) { + log.info("Initializing admin client for " + node.getContextRoot() + "/auth"); + return Keycloak.getInstance(node.getContextRoot() + "/auth", AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, Constants.ADMIN_CLI_CLIENT_ID); + } + + protected Keycloak getAdminClientFor(ContainerInfo node) { + Keycloak adminClient = backendAdminClients.get(node); + if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) { + adminClient = this.adminClient; + } + return adminClient; + } + + public void disableDcOnLoadBalancer(int dcIndex) { + log.infof("Disabling load balancer for dc=%d", dcIndex); + this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier())); + } + + /** + * Enables all started nodes in the given data center + * @param dcIndex + */ + public void enableDcOnLoadBalancer(int dcIndex) { + log.infof("Enabling load balancer for dc=%d", dcIndex); + final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); + if (! dcNodes.stream().anyMatch(ContainerInfo::isStarted)) { + log.warnf("No node is started in DC %d", dcIndex); + } else { + dcNodes.stream() + .filter(ContainerInfo::isStarted) + .forEach(c -> loadBalancerCtrl.enableBackendNodeByName(c.getQualifier())); + } + } + + public void disableLoadBalancerNode(int dcIndex, int nodeIndex) { + log.infof("Disabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex); + loadBalancerCtrl.disableBackendNodeByName(this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex).getQualifier()); + } + + public void enableLoadBalancerNode(int dcIndex, int nodeIndex) { + log.infof("Enabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex); + final ContainerInfo backendNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex); + if (backendNode == null) { + throw new IllegalArgumentException("Invalid node with index " + nodeIndex + " for DC " + dcIndex); + } + if (! backendNode.isStarted()) { + log.warnf("Node %s is not started in DC %d", backendNode.getQualifier(), dcIndex); + } + loadBalancerCtrl.enableBackendNodeByName(backendNode.getQualifier()); + } + + public Stream getManuallyStartedBackendNodes(int dcIndex) { + final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); + return dcNodes.stream().filter(ContainerInfo::isManual); + } + + public Stream getAutomaticallyStartedBackendNodes(int dcIndex) { + final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); + return dcNodes.stream().filter(c -> ! c.isManual()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java new file mode 100644 index 0000000000..45e757149f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java @@ -0,0 +1,152 @@ +/* + * 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.crossdc; + +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.events.admin.OperationType; +import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.Retry; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.ContainerInfo; +import org.keycloak.testsuite.page.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.util.AdminEventPaths; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.MailUtils; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.ws.rs.core.Response; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * + * @author hmlnarik + */ +public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected LoginPasswordUpdatePage passwordUpdatePage; + + @Page + protected ErrorPage errorPage; + + private String createUser(UserRepresentation userRep) { + Response response = realm.users().create(userRep); + String createdId = ApiUtil.getCreatedId(response); + response.close(); + + getCleanup().addUserId(createdId); + + return createdId; + } + + @Test + public void sendResetPasswordEmailSuccessWorksInCrossDc() throws IOException, MessagingException { + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + user.executeActionsEmail(actions); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); + + passwordUpdatePage.changePassword("new-pass", "new-pass"); + + assertEquals("Your account has been updated.", driver.getTitle()); + + disableDcOnLoadBalancer(0); + enableDcOnLoadBalancer(1); + + Retry.execute(() -> { + driver.navigate().to(link); + errorPage.assertCurrent(); + }, 3, 400); + } + + @Ignore("KEYCLOAK-5030") + @Test + public void sendResetPasswordEmailAfterNewNodeAdded() throws IOException, MessagingException { + disableDcOnLoadBalancer(1); + + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + user.executeActionsEmail(actions); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); + + passwordUpdatePage.changePassword("new-pass", "new-pass"); + + assertEquals("Your account has been updated.", driver.getTitle()); + + disableDcOnLoadBalancer(0); + getManuallyStartedBackendNodes(1) + .findFirst() + .ifPresent(c -> { + containerController.start(c.getQualifier()); + loadBalancerCtrl.enableBackendNodeByName(c.getQualifier()); + }); + + driver.navigate().to(link); + + errorPage.assertCurrent(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 9d801e51bf..2c80ddd288 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -107,15 +107,23 @@ "connectionsInfinispan": { "default": { + "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}", + "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:defaultNodeName}", "clustered": "${keycloak.connectionsInfinispan.clustered:false}", "async": "${keycloak.connectionsInfinispan.async:false}", "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}", "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}", "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}", - "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}", + "remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}" } }, + + "stickySessionEncoder": { + "infinispan": { + "nodeName": "${keycloak.stickySessionEncoder.nodeName,jboss.node.name:defaultNodeName}" + } + }, "truststore": { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index 6bc040f683..ecf986fada 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -103,6 +103,7 @@ ${backends.console.output} ${auth.server.backend1.management.port} ${auth.server.jboss.startup.timeout} + ${auth.server.backend1.port.offset} @@ -124,6 +125,7 @@ ${backends.console.output} ${auth.server.backend2.management.port} ${auth.server.jboss.startup.timeout} + ${auth.server.backend2.port.offset} @@ -165,6 +167,138 @@ + + + + + ${auth.server.undertow.crossdc} + org.jboss.as.arquillian.container.managed.ManagedDeployableContainer + ${cache.server.home} + standalone.xml + + -Djboss.socket.binding.port-offset=${cache.server.port.offset} + -Djboss.default.multicast.address=234.56.78.99 + -Djboss.node.name=cache-server + ${adapter.test.props} + ${auth.server.profile} + + + ${auth.server.memory.settings} + -Djava.net.preferIPv4Stack=true + + ${cache.server.console.output} + ${cache.server.management.port} + ${auth.server.jboss.startup.timeout} + + + + + + ${auth.server.undertow.crossdc} + org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer + localhost + ${auth.server.http.port} + 5 + auth-server-undertow-cross-dc-0.1=http://localhost:8101,auth-server-undertow-cross-dc-0.2-manual=http://localhost:8102,auth-server-undertow-cross-dc-1.1=http://localhost:8111,auth-server-undertow-cross-dc-1.2-manual=http://localhost:8112 + + + + + + ${auth.server.undertow.crossdc} + org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow + localhost + ${auth.server.http.port} + -79 + auth-server-undertow-cross-dc-0.1 + ${undertow.remote} + 0 + { + "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1", + "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0.1", + "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", + "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", + "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}", + "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}", + "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}", + "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}", + "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}" + } + + + + + ${auth.server.undertow.crossdc} + org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow + localhost + ${auth.server.http.port} + -78 + auth-server-undertow-cross-dc-0.2 + ${undertow.remote} + 0 + { + "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1", + "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0.2", + "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", + "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", + "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}", + "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}", + "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}", + "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}", + "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}" + } + + + + + + ${auth.server.undertow.crossdc} + org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow + localhost + ${auth.server.http.port} + -69 + auth-server-undertow-cross-dc-1.1 + ${undertow.remote} + 1 + { + "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2", + "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1.1", + "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", + "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", + "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}", + "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}", + "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}", + "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}", + "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}" + } + + + + + ${auth.server.undertow.crossdc} + org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow + localhost + ${auth.server.http.port} + -68 + auth-server-undertow-cross-dc-1.2 + ${undertow.remote} + 1 + { + "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2", + "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1.2", + "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", + "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", + "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}", + "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}", + "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}", + "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}", + "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}" + } + + + + + ${auth.server.cluster} diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 4e29ec3051..c14e7a758d 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -41,6 +41,7 @@ undertow true + true auth-server-${auth.server} ${containers.home}/${auth.server.container} @@ -65,7 +66,17 @@ false - + + infinispan + cache-server-${cache.server} + ${containers.home}/${cache.server.container} + 1010 + 11000 + true + localhost + 12232 + jdbc:h2:mem:test-dc-shared + ${project.build.directory}/examples @@ -289,6 +300,7 @@ wildfly true false + false standalone.xml ${auth.server.home}/standalone/configuration 1.3.173 @@ -307,6 +319,7 @@ eap true false + false standalone.xml ${auth.server.home}/standalone/configuration 1.3.173 @@ -319,6 +332,110 @@ + + cache-server-infinispan + + infinispan + true + ${cache.server.home}/standalone/configuration + + + + org.wildfly.arquillian + wildfly-arquillian-container-managed + + + + + + + + maven-enforcer-plugin + + + + enforce + + + + + + cache.server + (infinispan)|(jdg) + Profile "cache-server-infinispan" requires activation of profile "cache-server-infinispan" or "cache-server-jdg". + + + + + + + + maven-antrun-plugin + + + + + + maven-dependency-plugin + + + unpack-cache-server-infinispan + generate-resources + + unpack + + + + + org.keycloak.testsuite + integration-arquillian-servers-cache-server-infinispan + ${project.version} + zip + ${containers.home} + + + true + + + + + + maven-surefire-plugin + + + true + ${auth.server.undertow.crossdc} + + ${cache.server} + ${cache.server.port.offset} + ${cache.server.container} + ${cache.server.home} + ${cache.server.console.output} + ${cache.server.management.port} + + ${keycloak.connectionsInfinispan.remoteStorePort} + ${keycloak.connectionsInfinispan.remoteStoreServer} + + ${keycloak.connectionsJpa.url.crossdc} + + + + + + + + auth-server-profile