diff --git a/.gitignore b/.gitignore
index da1dad7b35..b5e00374d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ nbproject
# Logs and databases #
######################
*.log
+.attach_pid*
# Maven #
#########
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/assembly.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/assembly.xml
new file mode 100644
index 0000000000..b919fa1c95
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/assembly.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ auth-server-legacy
+
+
+ zip
+
+
+ false
+
+
+
+ ${auth.server.home}
+ auth-server-legacy
+
+ **/*.sh
+
+
+
+ ${auth.server.home}
+ auth-server-legacy
+
+ **/*.sh
+
+ 0755
+
+
+
+
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/pom.xml
new file mode 100644
index 0000000000..45dfdec434
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/pom.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+ org.keycloak.testsuite
+ integration-arquillian-servers-auth-server-jboss
+ 11.0.0-SNAPSHOT
+
+ 4.0.0
+
+ pom
+
+ integration-arquillian-servers-auth-server-legacy
+
+ Auth Server - Legacy
+
+
+ ${auth.server.legacy.version}
+ keycloak-${auth.server.legacy.version}
+
+
+
+
+
+ maven-enforcer-plugin
+
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ create-zip
+ package
+
+ single
+
+
+
+ assembly.xml
+
+ false
+
+
+
+
+
+
+
+
+
+ product
+
+
+ product
+
+
+
+ ${product.name}-${auth.server.legacy.filename.version}
+
+
+
+
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/src/.dont-delete b/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/src/.dont-delete
new file mode 100644
index 0000000000..63f93b0dd0
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/legacy/src/.dont-delete
@@ -0,0 +1 @@
+This file is to mark this Maven project as a valid option for building auth server artifact
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
index e72989c356..d2b263c8f5 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
@@ -655,6 +655,18 @@
+
+ auth-server-legacy
+
+
+ auth.server.legacy.version
+
+
+
+ legacy
+
+
+
auth-server-wildfly
diff --git a/testsuite/integration-arquillian/tests/base/.attach_pid34555 b/testsuite/integration-arquillian/tests/base/.attach_pid34555
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index e85aa1be00..eec96ddec0 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -126,6 +126,11 @@
org.apache.maven.resolver
maven-resolver-api
+
+ org.jboss
+ jandex
+ 2.1.3.Final
+
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 11b09c933c..44aebaa881 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
@@ -121,6 +121,8 @@ public class AuthServerTestEnricher {
public 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);
+ public static final String AUTH_SERVER_LEGACY = "auth-server-legacy";
+
public static final String AUTH_SERVER_BALANCER_DEFAULT = "auth-server-balancer";
public 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);
@@ -298,6 +300,15 @@ public class AuthServerTestEnricher {
suiteContext.addAuthServerBackendsInfo(0, c);
});
+ if (Boolean.parseBoolean(System.getProperty("auth.server.jboss.legacy"))) {
+ ContainerInfo legacy = containers.stream()
+ .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_LEGACY))
+ .findAny()
+ .orElseThrow(() -> new IllegalStateException("Not found legacy container: " + AUTH_SERVER_LEGACY));
+ updateWithAuthServerInfo(legacy, 500);
+ suiteContext.setLegacyAuthServerInfo(legacy);
+ }
+
if (suiteContext.getAuthServerBackendsInfo().isEmpty()) {
throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", AUTH_SERVER_BACKEND));
}
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 5be255f023..9129edc2a0 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
@@ -40,6 +40,7 @@ public final class SuiteContext {
private List authServerInfo = new LinkedList<>();
private final List> authServerBackendsInfo = new ArrayList<>();
+ private ContainerInfo legacyAuthServerInfo;
private final List cacheServersInfo = new ArrayList<>();
@@ -149,6 +150,14 @@ public final class SuiteContext {
authServerBackendsInfo.get(dcIndex).add(container);
}
+ public ContainerInfo getLegacyAuthServerInfo() {
+ return legacyAuthServerInfo;
+ }
+
+ public void setLegacyAuthServerInfo(ContainerInfo legacyAuthServerInfo) {
+ this.legacyAuthServerInfo = legacyAuthServerInfo;
+ }
+
public ContainerInfo getMigratedAuthServerInfo() {
return migratedAuthServerInfo;
}
@@ -205,6 +214,9 @@ public final class SuiteContext {
.append("\n");
getAuthServerBackendsInfo().forEach(bInfo -> sb.append(" Backend: ").append(bInfo).append(" - ").append(bInfo.getContextRoot().toExternalForm()).append("\n"));
+ if (Boolean.parseBoolean(System.getProperty("auth.server.jboss.legacy"))) {
+ sb.append(" Legacy: ").append(getLegacyAuthServerInfo()).append(" - ").append(getLegacyAuthServerInfo().getContextRoot().toExternalForm()).append("\n");
+ }
} else {
sb.append(getAuthServerInfo().getQualifier())
.append("\n");
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestClassProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestClassProvider.java
index ce609fa011..1a8877eb74 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestClassProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestClassProvider.java
@@ -23,6 +23,7 @@ public class TestClassProvider {
"/org/jboss/resteasy/client",
"/org/jboss/arquillian",
"/org/jboss/shrinkwrap",
+ "/org/jboss/jandex",
"/org/openqa/selenium"
};
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java
index d2459a0a33..59c7318831 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java
@@ -40,7 +40,6 @@ import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.WebArchive;
-import org.keycloak.common.Profile;
import org.keycloak.helpers.DropAllServlet;
import org.keycloak.testsuite.arquillian.ContainerInfo;
import org.keycloak.testsuite.arquillian.annotation.RestartContainer;
@@ -53,14 +52,7 @@ import org.wildfly.extras.creaper.core.online.OnlineOptions;
import java.io.File;
import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.Properties;
+import org.jboss.shrinkwrap.api.Archive;
import org.keycloak.testsuite.util.ContainerAssume;
/**
@@ -251,4 +243,23 @@ public class KeycloakContainerEventsController extends ContainerEventController
}
}
}
+
+ public static void deploy(Archive archive, ContainerInfo containerInfo) throws CommandFailedException, IOException {
+ ManagementClient.online(OnlineOptions
+ .standalone()
+ .hostAndPort("localhost", containerInfo.getContextRoot().getPort() + 1547)
+ .build())
+ .apply(new Deploy.Builder(
+ archive.as(ZipExporter.class).exportAsInputStream(),
+ archive.getName(),
+ true).build());
+ }
+
+ public static void undeploy(Archive archive, ContainerInfo containerInfo) throws CommandFailedException, IOException {
+ ManagementClient.online(OnlineOptions
+ .standalone()
+ .hostAndPort("localhost", containerInfo.getContextRoot().getPort() + 1547)
+ .build())
+ .apply(new Undeploy.Builder(archive.getName()).build());
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/cluster/MultiVersionClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/cluster/MultiVersionClusterTest.java
new file mode 100644
index 0000000000..163684aaff
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/cluster/MultiVersionClusterTest.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2020 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.migration.cluster;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+import org.apache.commons.io.FileUtils;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
+import org.infinispan.Cache;
+import org.infinispan.remoting.RemoteException;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Indexer;
+import org.jboss.modules.Module;
+import org.jboss.modules.ModuleClassLoader;
+import org.jboss.modules.ModuleLoader;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.After;
+import org.junit.Assert;
+import static org.junit.Assert.assertThat;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.infinispan.WrapperClusterEvent;
+import org.keycloak.common.util.reflections.Reflections;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
+import org.keycloak.testsuite.cluster.AbstractClusterTest;
+import static org.keycloak.testsuite.arquillian.containers.KeycloakContainerEventsController.deploy;
+import static org.keycloak.testsuite.arquillian.containers.KeycloakContainerEventsController.undeploy;
+import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rest.TestClassLoader;
+import org.keycloak.testsuite.runonserver.RunOnServerException;
+import org.keycloak.testsuite.runonserver.SerializationUtil;
+import org.keycloak.testsuite.util.DroneUtils;
+import org.keycloak.testsuite.util.OAuthClient;
+
+public class MultiVersionClusterTest extends AbstractClusterTest {
+
+ private static ContainerInfo currentNode;
+ private static ContainerInfo legacyNode;
+ private static boolean initialized = false;
+
+ @Page
+ protected LoginPage loginPage;
+
+ static class CacheValuesHolder {
+ private Map> values;
+
+ public CacheValuesHolder() {
+ }
+
+ public CacheValuesHolder(final Map> values) {
+ this.values = values;
+ }
+
+ public Map> getValues() {
+ return values;
+ }
+
+ public void setValues(Map> values) {
+ this.values = values;
+ }
+ }
+
+ @BeforeClass
+ public static void enabled() {
+ Assume.assumeThat(System.getProperty("auth.server.legacy.version"), notNullValue());
+ }
+
+ @Before
+ @Override
+ public void beforeClusterTest() {
+ if (!initialized) {
+ currentNode = backendNode(0);
+ legacyNode = suiteContext.getLegacyAuthServerInfo();
+ addAdminJsonFileToLegacy();
+
+ initialized = true;
+ }
+ startBackendNode(legacyNode);
+ startBackendNode(currentNode);
+ }
+
+ @After
+ public void after() {
+ killBackendNode(legacyNode);
+ killBackendNode(currentNode);
+ }
+
+ private JavaArchive deployment() {
+ return ShrinkWrap.create(JavaArchive.class, "negative.jar")
+ .addPackage("org/keycloak/testsuite")
+ .addClass(SerializableTestClass.class);
+ }
+
+ @Test
+ public void verifyFailureOnLegacy() throws Exception {
+
+ deploy(deployment(), currentNode);
+
+ try {
+ backendTestingClients.get(currentNode).server().run(session -> {
+ try {
+ Class> itShouldFail = Module.getContextModuleLoader().loadModule("deployment.negative.jar").getClassLoader()
+ .loadClassLocal(SerializableTestClass.class.getName());
+ session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME)
+ .put("itShouldFail", Reflections.newInstance(itShouldFail));
+ } catch (Exception ex) {
+ throw new RunOnServerException(ex);
+ }
+ });
+ } catch (Exception e) {
+ assertThat(e, instanceOf(RunOnServerException.class));
+ assertThat(e.getCause().getCause(), instanceOf(RemoteException.class));
+ } finally {
+ undeploy(deployment(), currentNode);
+ }
+ }
+
+ @Test
+ public void verifyFailureOnCurrent() throws Exception {
+
+ deploy(deployment(), legacyNode);
+
+ try {
+ backendTestingClients.get(legacyNode).server().run(session -> {
+ try {
+ Class> itShouldFail = Module.getContextModuleLoader().loadModule("deployment.negative.jar").getClassLoader()
+ .loadClassLocal(SerializableTestClass.class.getName());
+ session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME)
+ .put("itShouldFail", Reflections.newInstance(itShouldFail));
+ } catch (Exception ex) {
+ throw new RunOnServerException(ex);
+ }
+ });
+ } catch (Exception e) {
+ assertThat(e, instanceOf(RunOnServerException.class));
+ assertThat(e.getCause().getCause(), instanceOf(RemoteException.class));
+ } finally {
+ undeploy(deployment(), legacyNode);
+ }
+ }
+
+ /*
+ * Tests if legacy node remains usable (login) after current node connects to cluster
+ */
+ @Test
+ public void loginSuccessToLegacy() throws Exception {
+ String originalServerRoot = OAuthClient.SERVER_ROOT;
+ try {
+ OAuthClient.updateURLs(legacyNode.getContextRoot().toString());
+ OAuthClient oauth = new OAuthClient();
+ oauth.init(DroneUtils.getCurrentDriver());
+ oauth.realm(MASTER).clientId("account").redirectUri(legacyNode.getContextRoot().toString() + "/auth/realms/master/account/");
+
+ oauth.openLoginForm();
+ assertThat(DroneUtils.getCurrentDriver().getTitle(), containsString("Log in to "));
+ loginPage.login("admin", "admin");
+
+ assertThat("Login was not successful.", oauth.getCurrentQuery().get(OAuth2Constants.CODE), notNullValue());
+ } finally {
+ OAuthClient.updateURLs(originalServerRoot);
+ }
+ }
+
+ @Test
+ public void fromLegacyToCurrent() {
+ Map> expected = createCacheAndGetFromServer(legacyNode);
+ Map> actual = getFromServer(currentNode, SerializationUtil.encode(expected.keySet().toString()));
+ Assert.assertThat(actual, equalTo(expected));
+ }
+
+ @Test
+ public void fromCurrentToLegacy() {
+ Map> expected = createCacheAndGetFromServer(currentNode);
+ Map> actual = getFromServer(legacyNode, SerializationUtil.encode(expected.keySet().toString()));
+ Assert.assertThat(actual, equalTo(expected));
+ }
+
+ private void addAdminJsonFileToLegacy() {
+ try {
+ FileUtils.copyFile(new File("target/test-classes/keycloak-add-user.json"),
+ new File(System.getProperty("auth.server.legacy.home")
+ + "/standalone/configuration/keycloak-add-user.json"));
+ log.debug("Successfully added keycloak-add-user.json to " + System.getProperty("auth.server.legacy.home")
+ + "/standalone/configuration/keycloak-add-user.json");
+ } catch (IOException ex) {
+ throw new RuntimeException("Adding admin json file failed.", ex);
+ }
+ }
+
+ private Map> createCacheAndGetFromServer(ContainerInfo container) {
+ return backendTestingClients.get(container).server().fetch(session -> {
+ Map> result = new HashMap<>();
+
+ try {
+ Indexer indexer = new Indexer();
+ DotName serializeWith = DotName.createSimple("org.infinispan.commons.marshall.SerializeWith");
+
+ ModuleLoader contextModuleLoader = Module.getContextModuleLoader();
+ Module module = contextModuleLoader.loadModule("org.keycloak.keycloak-model-infinispan");
+ ModuleClassLoader classLoader = module.getClassLoader();
+
+ Enumeration resources = classLoader.getResources("org/keycloak");
+ while (resources.hasMoreElements()) {
+ URL nextElement = resources.nextElement();
+ Enumeration entries = new JarFile(nextElement.getFile().replace("file:", "").replace("!/org/keycloak", "")).entries();
+
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ if (entry.getName().endsWith(".class")) {
+ indexer.index(classLoader.getResourceAsStream(entry.getName()));
+ }
+ }
+ }
+
+ Cache
+
+ auth-server-legacy
+
+
+ auth.server.legacy.version
+
+
+
+ true
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ unpack-auth-server-legacy
+ generate-resources
+
+ unpack
+
+
+
+
+ org.keycloak.testsuite
+ integration-arquillian-servers-auth-server-legacy
+ ${project.version}
+ zip
+
+
+ ${containers.home}
+ true
+
+
+
+
+
+
+
+
clean-jpa