diff --git a/connections/infinispan/pom.xml b/connections/infinispan/pom.xml new file mode 100755 index 0000000000..788a53de1c --- /dev/null +++ b/connections/infinispan/pom.xml @@ -0,0 +1,33 @@ + + + + keycloak-parent + org.keycloak + 1.1.0-Alpha1-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-connections-infinispan + Keycloak Connections Infinispan + + + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.infinispan + infinispan-core + provided + + + diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java new file mode 100644 index 0000000000..28c947b0d5 --- /dev/null +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java @@ -0,0 +1,26 @@ +package org.keycloak.connections.infinispan; + +import org.infinispan.Cache; +import org.infinispan.manager.EmbeddedCacheManager; + +/** + * @author Stian Thorgersen + */ +public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider { + + private EmbeddedCacheManager cacheManager; + + public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public Cache getCache(String name) { + return cacheManager.getCache(name); + } + + @Override + public void close() { + } + +} diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java new file mode 100755 index 0000000000..3d0aab8900 --- /dev/null +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -0,0 +1,120 @@ +package org.keycloak.connections.infinispan; + +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; + +import javax.naming.InitialContext; + +/** + * @author Stian Thorgersen + */ +public class DefaultInfinispanConnectionProviderFactory implements InfinispanConnectionProviderFactory { + + protected static final Logger logger = Logger.getLogger(DefaultInfinispanConnectionProviderFactory.class); + + private Config.Scope config; + + private EmbeddedCacheManager cacheManager; + + private boolean containerManaged; + + @Override + public InfinispanConnectionProvider create(KeycloakSession session) { + lazyInit(); + + return new DefaultInfinispanConnectionProvider(cacheManager); + } + + @Override + public void close() { + if (cacheManager != null && !containerManaged) { + cacheManager.stop(); + } + cacheManager = null; + } + + @Override + public String getId() { + return "default"; + } + + @Override + public void init(Config.Scope config) { + this.config = config; + } + + private void lazyInit() { + if (cacheManager == null) { + synchronized (this) { + if (cacheManager == null) { + String cacheContainer = config.get("cacheContainer"); + if (cacheContainer != null) { + initContainerManaged(cacheContainer); + } else { + initEmbedded(); + } + } + } + } + } + + private void initContainerManaged(String cacheContainerLookup) { + try { + cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); + containerManaged = true; + + logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup); + } catch (Exception e) { + throw new RuntimeException("Failed to retrieve cache container", e); + } + } + + private void initEmbedded() { + GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); + if (config.getBoolean("transport", false)) { + gcb.transport().defaultTransport(); + } + cacheManager = new DefaultCacheManager(gcb.build()); + containerManaged = false; + + logger.debug("Started embedded Infinispan cache container"); + + cacheManager.defineConfiguration("sessions", createConfiguration("sessions")); + cacheManager.defineConfiguration("realms", createConfiguration("realms")); + } + + private Configuration createConfiguration(String cacheName) { + Config.Scope cacheConfig = config.scope("caches", cacheName); + ConfigurationBuilder cb = new ConfigurationBuilder(); + + String cacheMode = cacheConfig.get("cacheMode", "local"); + boolean async = cacheConfig.getBoolean("async", false); + + if (cacheMode.equalsIgnoreCase("replicated")) { + cb.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); + } else if (cacheMode.equalsIgnoreCase("distributed")) { + cb.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC); + + int owners = cacheConfig.getInt("owners", 2); + int segments = cacheConfig.getInt("segments", 60); + + cb.clustering().hash().numOwners(owners).numSegments(segments); + } else if (cacheMode.equalsIgnoreCase("invalidation")) { + cb.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC); + } else if (!cacheMode.equalsIgnoreCase("local")) { + throw new RuntimeException("Invalid cache mode " + cacheMode); + } + + logger.debugv("Configured cache {0} with mode={1}, async={2}", cacheName, cacheMode, async); + + return cb.build(); + } + +} diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java new file mode 100644 index 0000000000..098d00aae2 --- /dev/null +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -0,0 +1,13 @@ +package org.keycloak.connections.infinispan; + +import org.infinispan.Cache; +import org.keycloak.provider.Provider; + +/** + * @author Stian Thorgersen + */ +public interface InfinispanConnectionProvider extends Provider { + + Cache getCache(String name); + +} diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProviderFactory.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProviderFactory.java new file mode 100644 index 0000000000..8d411b4142 --- /dev/null +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProviderFactory.java @@ -0,0 +1,9 @@ +package org.keycloak.connections.infinispan; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface InfinispanConnectionProviderFactory extends ProviderFactory { +} diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionSpi.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionSpi.java new file mode 100644 index 0000000000..71f915d59c --- /dev/null +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.connections.infinispan; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class InfinispanConnectionSpi implements Spi { + + @Override + public String getName() { + return "connectionsInfinispan"; + } + + @Override + public Class getProviderClass() { + return InfinispanConnectionProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return InfinispanConnectionProviderFactory.class; + } + +} diff --git a/connections/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory b/connections/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory new file mode 100644 index 0000000000..f14e990b92 --- /dev/null +++ b/connections/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory @@ -0,0 +1 @@ +org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory \ No newline at end of file diff --git a/connections/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/connections/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..6db36cc493 --- /dev/null +++ b/connections/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.connections.infinispan.InfinispanConnectionSpi \ No newline at end of file diff --git a/connections/pom.xml b/connections/pom.xml index 5ea6c39a3d..a5b3bb2d6d 100755 --- a/connections/pom.xml +++ b/connections/pom.xml @@ -14,6 +14,7 @@ jpa + infinispan mongo diff --git a/core/src/main/java/org/keycloak/Config.java b/core/src/main/java/org/keycloak/Config.java index f26c85175b..1ce8fd0241 100755 --- a/core/src/main/java/org/keycloak/Config.java +++ b/core/src/main/java/org/keycloak/Config.java @@ -121,6 +121,17 @@ public class Config { return v != null ? Boolean.parseBoolean(v) : defaultValue; } + @Override + public Scope scope(String... scope) { + StringBuilder sb = new StringBuilder(); + sb.append(prefix + "."); + for (String s : scope) { + sb.append(s); + sb.append("."); + } + return new SystemPropertiesScope(sb.toString()); + } + } /** @@ -146,5 +157,7 @@ public class Config { Boolean getBoolean(String key, Boolean defaultValue); + Scope scope(String... scope); + } } diff --git a/core/src/main/java/org/keycloak/util/Time.java b/core/src/main/java/org/keycloak/util/Time.java index a7dc0fbd09..7da54f1f27 100644 --- a/core/src/main/java/org/keycloak/util/Time.java +++ b/core/src/main/java/org/keycloak/util/Time.java @@ -7,12 +7,18 @@ import java.util.Date; */ public class Time { + private static int offset; + public static int currentTime() { - return (int) (System.currentTimeMillis() / 1000); + return ((int) (System.currentTimeMillis() / 1000)) + offset; } public static Date toDate(int time) { return new Date(((long) time ) * 1000); } + public static void setOffset(int offset) { + Time.offset = offset; + } + } diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index acf732f655..0f7fed876d 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -26,6 +26,11 @@ keycloak-connections-jpa ${project.version} + + org.keycloak + keycloak-connections-infinispan + ${project.version} + org.keycloak keycloak-model-jpa @@ -46,6 +51,16 @@ keycloak-model-sessions-mongo ${project.version} + + org.keycloak + keycloak-model-sessions-infinispan + ${project.version} + + + org.keycloak + keycloak-invalidation-cache-infinispan + ${project.version} + org.keycloak keycloak-events-jpa diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java index d299a99b87..03a93d6f44 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java @@ -9,32 +9,18 @@ public interface UserSessionModel { String getId(); - void setId(String id); - UserModel getUser(); - void setUser(UserModel user); - String getLoginUsername(); - void setLoginUsername(String loginUsername); - String getIpAddress(); - void setIpAddress(String ipAddress); - String getAuthMethod(); - void setAuthMethod(String authMethod); - boolean isRememberMe(); - void setRememberMe(boolean rememberMe); - int getStarted(); - void setStarted(int started); - int getLastSessionRefresh(); void setLastSessionRefresh(int seconds); diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index 466717d05f..bf2c22dafe 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -28,7 +28,6 @@ public interface UserSessionProvider extends Provider { UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username); UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username); - List getAllUserLoginFailures(RealmModel realm); void onRealmRemoved(RealmModel realm); void onClientRemoved(RealmModel realm, ClientModel client); diff --git a/model/invalidation-cache/infinispan/pom.xml b/model/invalidation-cache/infinispan/pom.xml new file mode 100755 index 0000000000..f03fd7a78b --- /dev/null +++ b/model/invalidation-cache/infinispan/pom.xml @@ -0,0 +1,38 @@ + + + + keycloak-parent + org.keycloak + 1.1.0-Alpha1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + keycloak-invalidation-cache-infinispan + Keycloak Invalidation Cache Infinispan + + + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.keycloak + keycloak-invalidation-cache-model + ${project.version} + + + org.keycloak + keycloak-connections-infinispan + ${project.version} + + + org.infinispan + infinispan-core + provided + + + diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java new file mode 100755 index 0000000000..67b19274f7 --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java @@ -0,0 +1,42 @@ +package org.keycloak.models.cache.infinispan; + +import org.infinispan.Cache; +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.cache.CacheRealmProvider; +import org.keycloak.models.cache.CacheRealmProviderFactory; +import org.keycloak.models.cache.DefaultCacheRealmProvider; +import org.keycloak.models.cache.RealmCache; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Bill Burke + * @author Stian Thorgersen + */ +public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFactory { + + protected final ConcurrentHashMap realmLookup = new ConcurrentHashMap(); + + @Override + public CacheRealmProvider create(KeycloakSession session) { + Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache("realms"); + RealmCache realmCache = new InfinispanRealmCache(cache, realmLookup); + return new DefaultCacheRealmProvider(realmCache, session); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "infinispan"; + } + +} diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java new file mode 100755 index 0000000000..efbefca0c2 --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java @@ -0,0 +1,123 @@ +package org.keycloak.models.cache.infinispan; + +import org.infinispan.Cache; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; +import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent; +import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent; +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.cache.CacheUserProvider; +import org.keycloak.models.cache.CacheUserProviderFactory; +import org.keycloak.models.cache.DefaultCacheUserProvider; +import org.keycloak.models.cache.entities.CachedUser; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Stian Thorgersen + */ +public class InfinispanCacheUserProviderFactory implements CacheUserProviderFactory { + + protected InfinispanUserCache userCache; + + protected final RealmLookup usernameLookup = new RealmLookup(); + + protected final RealmLookup emailLookup = new RealmLookup(); + + @Override + public CacheUserProvider create(KeycloakSession session) { + lazyInit(session); + return new DefaultCacheUserProvider(userCache, session); + } + + private void lazyInit(KeycloakSession session) { + if (userCache == null) { + synchronized (this) { + if (userCache == null) { + Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache("users"); + cache.addListener(new CacheListener()); + userCache = new InfinispanUserCache(cache, usernameLookup, emailLookup); + } + } + } + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "infinispan"; + } + + @Listener + private class CacheListener { + + @CacheEntryCreated + public void userCreated(CacheEntryCreatedEvent event) { + if (!event.isPre() && event.getValue() != null) { + CachedUser cachedUser = event.getValue(); + String realm = cachedUser.getRealm(); + usernameLookup.put(realm, cachedUser.getUsername(), cachedUser.getId()); + if (cachedUser.getEmail() != null) { + emailLookup.put(realm, cachedUser.getEmail(), cachedUser.getId()); + } + } + } + + @CacheEntryRemoved + public void userRemoved(CacheEntryRemovedEvent event) { + if (event.isPre() && event.getValue() != null) { + CachedUser cachedUser = event.getValue(); + String realm = cachedUser.getRealm(); + usernameLookup.remove(realm, cachedUser.getUsername()); + if (cachedUser.getEmail() != null) { + emailLookup.remove(realm, cachedUser.getEmail()); + } + } + } + + } + + static class RealmLookup { + + protected final ConcurrentHashMap> lookup = new ConcurrentHashMap>(); + + public void put(String realm, String key, String value) { + ConcurrentHashMap map = lookup.get(realm); + if(map == null) { + map = new ConcurrentHashMap(); + ConcurrentHashMap p = lookup.putIfAbsent(realm, map); + if (p != null) { + map = p; + } + } + map.put(key, value); + } + + public String get(String realm, String key) { + ConcurrentHashMap map = lookup.get(realm); + return map != null ? map.get(key) : null; + } + + public void remove(String realm, String key) { + ConcurrentHashMap map = lookup.get(realm); + if (map != null) { + map.remove(key); + if (map.isEmpty()) { + lookup.remove(realm); + } + } + } + + } + +} diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java new file mode 100755 index 0000000000..a01a135273 --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java @@ -0,0 +1,166 @@ +package org.keycloak.models.cache.infinispan; + +import org.infinispan.Cache; +import org.jboss.logging.Logger; +import org.keycloak.models.cache.RealmCache; +import org.keycloak.models.cache.entities.CachedApplication; +import org.keycloak.models.cache.entities.CachedOAuthClient; +import org.keycloak.models.cache.entities.CachedRealm; +import org.keycloak.models.cache.entities.CachedRole; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Stian Thorgersen + */ +public class InfinispanRealmCache implements RealmCache { + + protected static final Logger logger = Logger.getLogger(InfinispanRealmCache.class); + + protected final Cache cache; + protected final ConcurrentHashMap realmLookup; + protected volatile boolean enabled = true; + + public InfinispanRealmCache(Cache cache, ConcurrentHashMap realmLookup) { + this.cache = cache; + this.realmLookup = realmLookup; + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + clear(); + this.enabled = enabled; + clear(); + } + + @Override + public CachedRealm getCachedRealm(String id) { + if (!enabled) return null; + return get(id, CachedRealm.class); + } + + @Override + public void invalidateCachedRealm(CachedRealm realm) { + logger.tracev("Invalidating realm {0}", realm.getId()); + cache.remove(realm.getId()); + realmLookup.remove(realm.getName()); + } + + @Override + public void invalidateCachedRealmById(String id) { + CachedRealm cached = (CachedRealm) cache.remove(id); + if (cached != null) realmLookup.remove(cached.getName()); + } + + @Override + public void addCachedRealm(CachedRealm realm) { + if (!enabled) return; + logger.tracev("Adding realm {0}", realm.getId()); + cache.put(realm.getId(), realm); + realmLookup.put(realm.getName(), realm.getId()); + } + + @Override + public CachedRealm getCachedRealmByName(String name) { + if (!enabled) return null; + String id = realmLookup.get(name); + return id != null ? getCachedRealm(id) : null; + } + + @Override + public CachedApplication getApplication(String id) { + if (!enabled) return null; + return get(id, CachedApplication.class); + } + + @Override + public void invalidateApplication(CachedApplication app) { + logger.tracev("Removing application {0}", app.getId()); + cache.remove(app.getId()); + } + + @Override + public void addCachedApplication(CachedApplication app) { + if (!enabled) return; + logger.tracev("Adding application {0}", app.getId()); + cache.put(app.getId(), app); + } + + @Override + public void invalidateCachedApplicationById(String id) { + logger.tracev("Removing application {0}", id); + cache.remove(id); + } + + @Override + public CachedOAuthClient getOAuthClient(String id) { + if (!enabled) return null; + return get(id, CachedOAuthClient.class); + } + + @Override + public void invalidateOAuthClient(CachedOAuthClient client) { + logger.tracev("Removing oauth client {0}", client.getId()); + cache.remove(client.getId()); + } + + @Override + public void addCachedOAuthClient(CachedOAuthClient client) { + if (!enabled) return; + logger.tracev("Adding oauth client {0}", client.getId()); + cache.put(client.getId(), client); + } + + @Override + public void invalidateCachedOAuthClientById(String id) { + logger.tracev("Removing oauth client {0}", id); + cache.remove(id); + } + + @Override + public CachedRole getRole(String id) { + if (!enabled) return null; + return get(id, CachedRole.class); + } + + @Override + public void invalidateRole(CachedRole role) { + logger.tracev("Removing role {0}", role.getId()); + cache.remove(role); + } + + @Override + public void invalidateRoleById(String id) { + logger.tracev("Removing role {0}", id); + cache.remove(id); + } + + @Override + public void addCachedRole(CachedRole role) { + if (!enabled) return; + logger.tracev("Adding role {0}", role.getId()); + cache.put(role.getId(), role); + } + + @Override + public void invalidateCachedRoleById(String id) { + logger.tracev("Removing role {0}", id); + cache.remove(id); + } + + private T get(String id, Class type) { + Object o = cache.get(id); + return o != null && type.isInstance(o) ? type.cast(o) : null; + } + +} diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCache.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCache.java new file mode 100755 index 0000000000..7efa174076 --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCache.java @@ -0,0 +1,95 @@ +package org.keycloak.models.cache.infinispan; + +import org.infinispan.Cache; +import org.jboss.logging.Logger; +import org.keycloak.models.cache.UserCache; +import org.keycloak.models.cache.entities.CachedUser; + +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class InfinispanUserCache implements UserCache { + + protected static final Logger logger = Logger.getLogger(InfinispanRealmCache.class); + + protected volatile boolean enabled = true; + + protected final Cache cache; + + protected final InfinispanCacheUserProviderFactory.RealmLookup usernameLookup; + + protected final InfinispanCacheUserProviderFactory.RealmLookup emailLookup; + + public InfinispanUserCache(Cache cache, InfinispanCacheUserProviderFactory.RealmLookup usernameLookup, InfinispanCacheUserProviderFactory.RealmLookup emailLookup) { + this.cache = cache; + this.usernameLookup = usernameLookup; + this.emailLookup = emailLookup; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + clear(); + this.enabled = enabled; + clear(); + } + + @Override + public CachedUser getCachedUser(String realmId, String id) { + if (realmId == null || id == null) return null; + CachedUser user = cache.get(id); + return user != null && realmId.equals(user.getRealm()) ? user : null; + } + + @Override + public void invalidateCachedUser(String realmId, CachedUser user) { + logger.tracev("Invalidating user {0}", user.getId()); + cache.remove(user.getId()); + } + + @Override + public void invalidateCachedUserById(String realmId, String id) { + logger.tracev("Invalidating user {0}", id); + cache.remove(id); + } + + @Override + public void addCachedUser(String realmId, CachedUser user) { + logger.tracev("Adding user {0}", user.getId()); + cache.put(user.getId(), user); + } + + @Override + public CachedUser getCachedUserByUsername(String realmId, String name) { + String id = usernameLookup.get(realmId, name); + return id != null ? getCachedUser(realmId, id) : null; + } + + @Override + public CachedUser getCachedUserByEmail(String realmId, String email) { + String id = emailLookup.get(realmId, email); + return id != null ? getCachedUser(realmId, id) : null; + } + + @Override + public void invalidateRealmUsers(String realmId) { + logger.tracev("Invalidating users for realm {0}", realmId); + for (Map.Entry u : cache.entrySet()) { + if (u.getValue().getRealm().equals(realmId)) { + cache.remove(u.getKey()); + } + } + } + + @Override + public void clear() { + cache.clear(); + } + +} diff --git a/model/invalidation-cache/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory b/model/invalidation-cache/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory new file mode 100755 index 0000000000..d354ddfacc --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory @@ -0,0 +1 @@ +org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory \ No newline at end of file diff --git a/model/invalidation-cache/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheUserProviderFactory b/model/invalidation-cache/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheUserProviderFactory new file mode 100755 index 0000000000..499bb2080e --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheUserProviderFactory @@ -0,0 +1 @@ +org.keycloak.models.cache.infinispan.InfinispanCacheUserProviderFactory \ No newline at end of file diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 1e247a7aa0..ca9a24c0ff 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -18,6 +18,7 @@ import java.util.Set; */ public class CachedUser { private String id; + private String realm; private String username; private String firstName; private String lastName; @@ -34,6 +35,7 @@ public class CachedUser { public CachedUser(RealmModel realm, UserModel user) { this.id = user.getId(); + this.realm = realm.getId(); this.username = user.getUsername(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); @@ -54,6 +56,10 @@ public class CachedUser { return id; } + public String getRealm() { + return realm; + } + public String getUsername() { return username; } diff --git a/model/invalidation-cache/pom.xml b/model/invalidation-cache/pom.xml index 09495fc2da..e24a15d6f3 100755 --- a/model/invalidation-cache/pom.xml +++ b/model/invalidation-cache/pom.xml @@ -26,5 +26,6 @@ model-adapters + infinispan diff --git a/model/pom.xml b/model/pom.xml index ec811365ed..6296e15989 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -32,5 +32,6 @@ sessions-jpa sessions-mem sessions-mongo + sessions-infinispan diff --git a/model/sessions-infinispan/pom.xml b/model/sessions-infinispan/pom.xml new file mode 100755 index 0000000000..9462f7aa34 --- /dev/null +++ b/model/sessions-infinispan/pom.xml @@ -0,0 +1,41 @@ + + + + keycloak-parent + org.keycloak + 1.1.0-Alpha1-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-model-sessions-infinispan + Keycloak Model Sessions Infinispan + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-model-api + ${project.version} + provided + + + org.keycloak + keycloak-connections-infinispan + ${project.version} + provided + + + org.infinispan + infinispan-core + provided + + + diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java new file mode 100644 index 0000000000..2c6addc2d7 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java @@ -0,0 +1,151 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.util.HashMap; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionAdapter implements ClientSessionModel { + + private KeycloakSession session; + private InfinispanUserSessionProvider provider; + private Cache cache; + private RealmModel realm; + private ClientSessionEntity entity; + + public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, ClientSessionEntity entity) { + this.session = session; + this.provider = provider; + this.cache = cache; + this.realm = realm; + this.entity = entity; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public ClientModel getClient() { + return realm.findClientById(entity.getClient()); + } + + @Override + public UserSessionModel getUserSession() { + return entity.getUserSession() != null ? provider.getUserSession(realm, entity.getUserSession()) : null; + } + + @Override + public void setUserSession(UserSessionModel userSession) { + if (entity.getUserSession() != null) { + if (entity.getUserSession().equals(userSession.getId())) { + return; + } else { + provider.dettachSession(userSession, this); + } + } else { + provider.attachSession(userSession, this); + } + + entity.setUserSession(userSession.getId()); + update(); + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public void setRedirectUri(String uri) { + entity.setRedirectUri(uri); + update(); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + update(); + } + + @Override + public Action getAction() { + return entity.getAction(); + } + + @Override + public void setAction(Action action) { + entity.setAction(action); + update(); + } + + @Override + public Set getRoles() { + return entity.getRoles(); + } + + @Override + public void setRoles(Set roles) { + entity.setRoles(roles); + update(); + } + + @Override + public String getAuthMethod() { + return entity.getAuthMethod(); + } + + @Override + public void setAuthMethod(String authMethod) { + entity.setAuthMethod(authMethod); + update(); + } + + @Override + public String getNote(String name) { + return entity.getNotes() != null ? entity.getNotes().get(name) : null; + } + + @Override + public void setNote(String name, String value) { + if (entity.getNotes() == null) { + entity.setNotes(new HashMap()); + } + entity.getNotes().put(name, value); + update(); + } + + @Override + public void removeNote(String name) { + if (entity.getNotes() != null) { + entity.getNotes().remove(name); + update(); + } + } + + void update() { + provider.getTx().replace(cache, entity.getId(), entity); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java new file mode 100644 index 0000000000..033fe534bc --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -0,0 +1,439 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.distexec.mapreduce.MapReduceTask; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakTransaction; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UsernameLoginFailureModel; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper; +import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer; +import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer; +import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper; +import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.util.Time; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class InfinispanUserSessionProvider implements UserSessionProvider { + + private final KeycloakSession session; + private final Cache sessionCache; + private final Cache loginFailureCache; + private final InfinispanKeycloakTransaction tx; + + public InfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache loginFailureCache) { + this.session = session; + this.sessionCache = sessionCache; + this.loginFailureCache = loginFailureCache; + this.tx = new InfinispanKeycloakTransaction(); + + session.getTransaction().enlistAfterCompletion(tx); + } + + @Override + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) { + String id = KeycloakModelUtils.generateId(); + + ClientSessionEntity entity = new ClientSessionEntity(); + entity.setId(id); + entity.setRealm(realm.getId()); + entity.setTimestamp(Time.currentTime()); + entity.setClient(client.getId()); + + tx.put(sessionCache, id, entity); + + return wrap(realm, entity); + } + + @Override + public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { + String id = KeycloakModelUtils.generateId(); + + UserSessionEntity entity = new UserSessionEntity(); + entity.setId(id); + entity.setRealm(realm.getId()); + entity.setUser(user.getId()); + entity.setLoginUsername(loginUsername); + entity.setIpAddress(ipAddress); + entity.setAuthMethod(authMethod); + entity.setRememberMe(rememberMe); + + int currentTime = Time.currentTime(); + + entity.setStarted(currentTime); + entity.setLastSessionRefresh(currentTime); + + tx.put(sessionCache, id, entity); + + return wrap(realm, entity); + } + + @Override + public ClientSessionModel getClientSession(RealmModel realm, String id) { + ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id); + return wrap(realm, entity); + } + + @Override + public ClientSessionModel getClientSession(String id) { + ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id); + if (entity != null) { + RealmModel realm = session.realms().getRealm(entity.getRealm()); + return wrap(realm, entity); + } + return null; + } + + @Override + public UserSessionModel getUserSession(RealmModel realm, String id) { + UserSessionEntity entity = (UserSessionEntity) sessionCache.get(id); + return wrap(realm, entity); + } + + @Override + public List getUserSessions(RealmModel realm, UserModel user) { + Map sessions = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + + return wrapUserSessions(realm, sessions.values()); + } + + @Override + public List getUserSessions(RealmModel realm, ClientModel client) { + return getUserSessions(realm, client, -1, -1); + } + + @Override + public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { + Map map = new MapReduceTask(sessionCache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) + .reducedWith(new LargestResultReducer()) + .execute(); + + List> sessionTimestamps = new LinkedList>(map.entrySet()); + + Collections.sort(sessionTimestamps, new Comparator>() { + @Override + public int compare(Map.Entry e1, Map.Entry e2) { + return e1.getValue().compareTo(e2.getValue()); + } + }); + + if (firstResult != -1 || maxResults == -1) { + if (firstResult == -1) { + firstResult = 0; + } + + if (maxResults == -1) { + maxResults = Integer.MAX_VALUE; + } + + if (firstResult > sessionTimestamps.size()) { + return Collections.emptyList(); + } + + int toIndex = (firstResult + maxResults) < sessionTimestamps.size() ? firstResult + maxResults : sessionTimestamps.size(); + sessionTimestamps = sessionTimestamps.subList(firstResult, toIndex); + } + + List userSessions = new LinkedList(); + for (Map.Entry e : sessionTimestamps) { + UserSessionEntity userSessionEntity = (UserSessionEntity) sessionCache.get(e.getKey()); + if (userSessionEntity != null) { + userSessions.add(wrap(realm, userSessionEntity)); + } + } + + return userSessions; + } + + @Override + public int getActiveUserSessions(RealmModel realm, ClientModel client) { + Map map = new MapReduceTask(sessionCache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) + .reducedWith(new LargestResultReducer()).execute(); + + return map.size(); + } + + @Override + public void removeUserSession(RealmModel realm, UserSessionModel session) { + removeUserSession(realm, session.getId()); + } + + @Override + public void removeUserSessions(RealmModel realm, UserModel user) { + Map sessions = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : sessions.keySet()) { + removeUserSession(realm, id); + } + } + + @Override + public void removeExpiredUserSessions(RealmModel realm) { + int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan(); + int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); + + Map map = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).expired(expired, expiredRefresh).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + removeUserSession(realm, id); + } + } + + @Override + public void removeUserSessions(RealmModel realm) { + Map ids = new MapReduceTask(sessionCache) + .mappedWith(SessionMapper.create(realm.getId()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : ids.keySet()) { + sessionCache.remove(id); + } + } + + @Override + public UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username) { + return wrap(loginFailureCache.get(realm.getId() + ":" + username)); + } + + @Override + public UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username) { + LoginFailureEntity entity = new LoginFailureEntity(); + entity.setRealm(realm.getId()); + entity.setUsername(username); + tx.put(loginFailureCache, entity.getId(), entity); + return wrap(entity); + } + + @Override + public void onRealmRemoved(RealmModel realm) { + removeUserSessions(realm); + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + Map map = new MapReduceTask(sessionCache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(sessionCache, id); + } + } + + @Override + public void onUserRemoved(RealmModel realm, UserModel user) { + removeUserSessions(realm, user); + } + + @Override + public void close() { + } + + void attachSession(UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); + String clientSessionId = clientSession.getId(); + if (entity.getClientSessions() == null) { + entity.setClientSessions(new HashSet()); + } + if (!entity.getClientSessions().contains(clientSessionId)) { + entity.getClientSessions().add(clientSessionId); + tx.replace(sessionCache, entity.getId(), entity); + } + } + + void dettachSession(UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); + String clientSessionId = clientSession.getId(); + if (entity.getClientSessions() != null && entity.getClientSessions().contains(clientSessionId)) { + entity.getClientSessions().remove(clientSessionId); + if (entity.getClientSessions().isEmpty()) { + entity.setClientSessions(null); + } + tx.replace(sessionCache, entity.getId(), entity); + } + } + + void removeUserSession(RealmModel realm, String userSessionId) { + tx.remove(sessionCache, userSessionId); + + Map map = new MapReduceTask(sessionCache) + .mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(sessionCache, id); + } + } + + InfinispanKeycloakTransaction getTx() { + return tx; + } + + UserSessionModel wrap(RealmModel realm, UserSessionEntity entity) { + return entity != null ? new UserSessionAdapter(session, this, sessionCache, realm, entity) : null; + } + + List wrapUserSessions(RealmModel realm, Collection entities) { + List models = new LinkedList(); + for (UserSessionEntity e : entities) { + models.add(wrap(realm, e)); + } + return models; + } + + ClientSessionModel wrap(RealmModel realm, ClientSessionEntity entity) { + return entity != null ? new ClientSessionAdapter(session, this, sessionCache, realm, entity) : null; + } + + + UsernameLoginFailureModel wrap(LoginFailureEntity entity) { + return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, entity) : null; + } + + List wrapClientSessions(RealmModel realm, Collection entities) { + List models = new LinkedList(); + for (ClientSessionEntity e : entities) { + models.add(wrap(realm, e)); + } + return models; + } + + class InfinispanKeycloakTransaction implements KeycloakTransaction { + + private boolean active; + private boolean rollback; + private Map tasks = new HashMap(); + + @Override + public void begin() { + active = true; + } + + @Override + public void commit() { + if (rollback) { + throw new RuntimeException("Rollback only!"); + } + + for (CacheTask task : tasks.values()) { + task.execute(); + } + } + + @Override + public void rollback() { + tasks.clear(); + } + + @Override + public void setRollbackOnly() { + rollback = true; + } + + @Override + public boolean getRollbackOnly() { + return rollback; + } + + @Override + public boolean isActive() { + return active; + } + + public void put(Cache cache, String key, Object value) { + if (tasks.containsKey(key)) { + throw new IllegalStateException("Can't add session: task in progress for session"); + } else { + tasks.put(key, new CacheTask(cache, CacheOperation.ADD, key, value)); + } + } + + public void replace(Cache cache, String key, Object value) { + CacheTask current = tasks.get(key); + if (current != null) { + switch (current.operation) { + case ADD: + case REPLACE: + current.value = value; + return; + case REMOVE: + throw new IllegalStateException("Can't remove session: task in progress for session"); + } + } else { + tasks.put(key, new CacheTask(cache, CacheOperation.ADD, key, value)); + } + } + + public void remove(Cache cache, String key) { + tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null)); + } + + public class CacheTask { + private Cache cache; + private CacheOperation operation; + private String key; + private Object value; + + public CacheTask(Cache cache, CacheOperation operation, String key, Object value) { + this.cache = cache; + this.operation = operation; + this.key = key; + this.value = value; + } + + public void execute() { + switch (operation) { + case ADD: + cache.put(key, value); + break; + case REMOVE: + cache.remove(key); + break; + case REPLACE: + cache.replace(key, value); + } + } + } + + } + + public enum CacheOperation { + ADD, REMOVE, REPLACE + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java new file mode 100644 index 0000000000..40287af857 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -0,0 +1,49 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import javax.naming.InitialContext; + +/** + * @author Stian Thorgersen + */ +public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory { + + private static final String SESSION_CACHE_NAME = "sessions"; + private static final String LOGIN_FAILURE_CACHE_NAME = "loginFailures"; + + @Override + public UserSessionProvider create(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache cache = connections.getCache(SESSION_CACHE_NAME); + Cache loginFailures = connections.getCache(LOGIN_FAILURE_CACHE_NAME); + return new InfinispanUserSessionProvider(session, cache, loginFailures); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "infinispan"; + } + +} + diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java new file mode 100755 index 0000000000..7b3000ebca --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -0,0 +1,122 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.distexec.mapreduce.MapReduceTask; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper; +import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class UserSessionAdapter implements UserSessionModel { + + private final KeycloakSession session; + + private final InfinispanUserSessionProvider provider; + + private final Cache cache; + + private final RealmModel realm; + + private final UserSessionEntity entity; + + public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, UserSessionEntity entity) { + this.session = session; + this.provider = provider; + this.cache = cache; + this.realm = realm; + this.entity = entity; + } + + public String getId() { + return entity.getId(); + } + + public UserModel getUser() { + return session.users().getUserById(entity.getUser(), realm); + } + + @Override + public String getLoginUsername() { + return entity.getLoginUsername(); + } + + public String getIpAddress() { + return entity.getIpAddress(); + } + + @Override + public String getAuthMethod() { + return entity.getAuthMethod(); + } + + @Override + public boolean isRememberMe() { + return entity.isRememberMe(); + } + + public int getStarted() { + return entity.getStarted(); + } + + public int getLastSessionRefresh() { + return entity.getLastSessionRefresh(); + } + + public void setLastSessionRefresh(int lastSessionRefresh) { + entity.setLastSessionRefresh(lastSessionRefresh); + update(); + } + + @Override + public List getClientSessions() { + if (entity.getClientSessions() != null) { + List clientSessions = new LinkedList(); + for (String c : entity.getClientSessions()) { + ClientSessionEntity clientSession = (ClientSessionEntity) cache.get(c); + if (clientSession != null) { + clientSessions.add(clientSession); + } + } + return provider.wrapClientSessions(realm, clientSessions); + } else { + return Collections.emptyList(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof UserSessionModel)) return false; + + UserSessionModel that = (UserSessionModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + UserSessionEntity getEntity() { + return entity; + } + + void update() { + provider.getTx().replace(cache, entity.getId(), entity); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UsernameLoginFailureAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UsernameLoginFailureAdapter.java new file mode 100755 index 0000000000..2de708936d --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UsernameLoginFailureAdapter.java @@ -0,0 +1,81 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.keycloak.models.UsernameLoginFailureModel; +import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; + +/** + * @author Stian Thorgersen + */ +public class UsernameLoginFailureAdapter implements UsernameLoginFailureModel { + + private InfinispanUserSessionProvider provider; + private Cache cache; + private LoginFailureEntity entity; + + public UsernameLoginFailureAdapter(InfinispanUserSessionProvider provider, Cache cache, LoginFailureEntity entity) { + this.provider = provider; + this.cache = cache; + this.entity = entity; + } + + @Override + public String getUsername() { + return entity.getUsername(); + } + + @Override + public int getFailedLoginNotBefore() { + return entity.getFailedLoginNotBefore(); + } + + @Override + public void setFailedLoginNotBefore(int notBefore) { + entity.setFailedLoginNotBefore(notBefore); + update(); + } + + @Override + public int getNumFailures() { + return entity.getNumFailures(); + } + + @Override + public void incrementFailures() { + entity.setNumFailures(getNumFailures() + 1); + update(); + } + + @Override + public void clearFailures() { + entity.setNumFailures(0); + update(); + } + + @Override + public long getLastFailure() { + return entity.getLastFailure(); + } + + @Override + public void setLastFailure(long lastFailure) { + entity.setLastFailure(lastFailure); + update(); + } + + @Override + public String getLastIPFailure() { + return entity.getLastIPFailure(); + } + + @Override + public void setLastIPFailure(String ip) { + entity.setLastIPFailure(ip); + update(); + } + + void update() { + provider.getTx().replace(cache, entity.getId(), entity); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java new file mode 100644 index 0000000000..bfcf1c6f3c --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java @@ -0,0 +1,102 @@ +package org.keycloak.models.sessions.infinispan.entities; + +import org.keycloak.models.ClientSessionModel; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionEntity extends SessionEntity { + + private String client; + + private String userSession; + + private String authMethod; + + private String redirectUri; + + private String state; + + private int timestamp; + + private ClientSessionModel.Action action; + + private Set roles; + private Map notes; + + public String getClient() { + return client; + } + + public void setClient(String client) { + this.client = client; + } + + public String getUserSession() { + return userSession; + } + + public void setUserSession(String userSession) { + this.userSession = userSession; + } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public ClientSessionModel.Action getAction() { + return action; + } + + public void setAction(ClientSessionModel.Action action) { + this.action = action; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public Map getNotes() { + return notes; + } + + public void setNotes(Map notes) { + this.notes = notes; + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java new file mode 100644 index 0000000000..09bfca661e --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java @@ -0,0 +1,67 @@ +package org.keycloak.models.sessions.infinispan.entities; + +/** + * @author Stian Thorgersen + */ +public class LoginFailureEntity { + + private String username; + private String realm; + private int failedLoginNotBefore; + private int numFailures; + private long lastFailure; + private String lastIPFailure; + + public String getId() { + return realm + ":" + username; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public int getFailedLoginNotBefore() { + return failedLoginNotBefore; + } + + public void setFailedLoginNotBefore(int failedLoginNotBefore) { + this.failedLoginNotBefore = failedLoginNotBefore; + } + + public int getNumFailures() { + return numFailures; + } + + public void setNumFailures(int numFailures) { + this.numFailures = numFailures; + } + + public long getLastFailure() { + return lastFailure; + } + + public void setLastFailure(long lastFailure) { + this.lastFailure = lastFailure; + } + + public String getLastIPFailure() { + return lastIPFailure; + } + + public void setLastIPFailure(String lastIPFailure) { + this.lastIPFailure = lastIPFailure; + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java new file mode 100644 index 0000000000..5d6fc3cd64 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java @@ -0,0 +1,29 @@ +package org.keycloak.models.sessions.infinispan.entities; + +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +public class SessionEntity implements Serializable { + + private String id; + + private String realm; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java new file mode 100644 index 0000000000..3300ae666f --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java @@ -0,0 +1,89 @@ +package org.keycloak.models.sessions.infinispan.entities; + +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class UserSessionEntity extends SessionEntity { + + private String user; + + private String loginUsername; + + private String ipAddress; + + private String authMethod; + + private boolean rememberMe; + + private int started; + + private int lastSessionRefresh; + + private Set clientSessions; + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getLoginUsername() { + return loginUsername; + } + + public void setLoginUsername(String loginUsername) { + this.loginUsername = loginUsername; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(boolean rememberMe) { + this.rememberMe = rememberMe; + } + + public int getStarted() { + return started; + } + + public void setStarted(int started) { + this.started = started; + } + + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + public void setLastSessionRefresh(int lastSessionRefresh) { + this.lastSessionRefresh = lastSessionRefresh; + } + + public Set getClientSessions() { + return clientSessions; + } + + public void setClientSessions(Set clientSessions) { + this.clientSessions = clientSessions; + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java new file mode 100644 index 0000000000..7329d3fa88 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java @@ -0,0 +1,88 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import org.infinispan.distexec.mapreduce.Collector; +import org.infinispan.distexec.mapreduce.Mapper; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionMapper implements Mapper, Serializable { + + public ClientSessionMapper(String realm) { + this.realm = realm; + } + + private enum EmitValue { + KEY, ENTITY, USER_SESSION_AND_TIMESTAMP + } + + private String realm; + + private EmitValue emit = EmitValue.ENTITY; + + private String client; + + private String userSession; + + public static ClientSessionMapper create(String realm) { + return new ClientSessionMapper(realm); + } + + public ClientSessionMapper emitKey() { + emit = EmitValue.KEY; + return this; + } + + public ClientSessionMapper emitUserSessionAndTimestamp() { + emit = EmitValue.USER_SESSION_AND_TIMESTAMP; + return this; + } + + public ClientSessionMapper client(String client) { + this.client = client; + return this; + } + + public ClientSessionMapper userSession(String userSession) { + this.userSession = userSession; + return this; + } + + @Override + public void map(String key, SessionEntity e, Collector collector) { + if (!realm.equals(e.getRealm())) { + return; + } + + if (!(e instanceof ClientSessionEntity)) { + return; + } + + ClientSessionEntity entity = (ClientSessionEntity) e; + + if (client != null && !entity.getClient().equals(client)) { + return; + } + + if (userSession != null && !userSession.equals(entity.getUserSession())) { + return; + } + + switch (emit) { + case KEY: + collector.emit(key, key); + break; + case ENTITY: + collector.emit(key, entity); + break; + case USER_SESSION_AND_TIMESTAMP: + collector.emit(entity.getUserSession(), entity.getTimestamp()); + break; + } + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/FirstResultReducer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/FirstResultReducer.java new file mode 100644 index 0000000000..622d98f00c --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/FirstResultReducer.java @@ -0,0 +1,18 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import org.infinispan.distexec.mapreduce.Reducer; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * @author Stian Thorgersen + */ +public class FirstResultReducer implements Reducer, Serializable { + + @Override + public Object reduce(Object reducedKey, Iterator itr) { + return itr.next(); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java new file mode 100644 index 0000000000..33b0d07a21 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java @@ -0,0 +1,24 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import org.infinispan.distexec.mapreduce.Reducer; + +import java.util.Iterator; + +/** + * @author Stian Thorgersen + */ +public class LargestResultReducer implements Reducer { + + @Override + public Integer reduce(String reducedKey, Iterator itr) { + Integer largest = itr.next(); + while (itr.hasNext()) { + Integer next = itr.next(); + if (next > largest) { + largest = next; + } + } + return largest; + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/SessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/SessionMapper.java new file mode 100644 index 0000000000..747d094558 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/SessionMapper.java @@ -0,0 +1,52 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import org.infinispan.distexec.mapreduce.Collector; +import org.infinispan.distexec.mapreduce.Mapper; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +public class SessionMapper implements Mapper, Serializable { + + public SessionMapper(String realm) { + this.realm = realm; + } + + private enum EmitValue { + KEY, ENTITY + } + + private String realm; + + private EmitValue emit = EmitValue.ENTITY; + + public static SessionMapper create(String realm) { + return new SessionMapper(realm); + } + + public SessionMapper emitKey() { + emit = EmitValue.KEY; + return this; + } + + @Override + public void map(String key, SessionEntity e, Collector collector) { + if (!realm.equals(e.getRealm())) { + return; + } + + switch (emit) { + case KEY: + collector.emit(key, key); + break; + case ENTITY: + collector.emit(key, e); + break; + } + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java new file mode 100644 index 0000000000..3c28284830 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java @@ -0,0 +1,84 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import org.infinispan.distexec.mapreduce.Collector; +import org.infinispan.distexec.mapreduce.Mapper; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +public class UserSessionMapper implements Mapper, Serializable { + + public UserSessionMapper(String realm) { + this.realm = realm; + } + + private enum EmitValue { + KEY, ENTITY + } + + private String realm; + + private EmitValue emit = EmitValue.ENTITY; + + private String user; + + private Long expired; + + private Long expiredRefresh; + + public static UserSessionMapper create(String realm) { + return new UserSessionMapper(realm); + } + + public UserSessionMapper emitKey() { + emit = EmitValue.KEY; + return this; + } + + public UserSessionMapper user(String user) { + this.user = user; + return this; + } + + public UserSessionMapper expired(long expired, long expiredRefresh) { + this.expired = expired; + this.expiredRefresh = expiredRefresh; + return this; + } + + @Override + public void map(String key, SessionEntity e, Collector collector) { + if (!(e instanceof UserSessionEntity)) { + return; + } + + UserSessionEntity entity = (UserSessionEntity) e; + + if (!realm.equals(entity.getRealm())) { + return; + } + + if (user != null && !entity.getUser().equals(user)) { + return; + } + + if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) { + return; + } + + switch (emit) { + case KEY: + collector.emit(key, key); + break; + case ENTITY: + collector.emit(key, entity); + break; + } + } + +} diff --git a/model/sessions-infinispan/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory b/model/sessions-infinispan/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory new file mode 100644 index 0000000000..2be2c7954c --- /dev/null +++ b/model/sessions-infinispan/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory @@ -0,0 +1 @@ +org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory \ No newline at end of file diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java index 335a48f770..a3e7297462 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java @@ -86,17 +86,6 @@ public class JpaUserSessionProvider implements UserSessionProvider { return new UsernameLoginFailureAdapter(entity); } - @Override - public List getAllUserLoginFailures(RealmModel realm) { - TypedQuery query = em.createNamedQuery("getAllFailures", UsernameLoginFailureEntity.class); - List entities = query.getResultList(); - List models = new ArrayList(); - for (UsernameLoginFailureEntity entity : entities) { - models.add(new UsernameLoginFailureAdapter(entity)); - } - return models; - } - @Override public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { UserSessionEntity entity = new UserSessionEntity(); diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java index 3261b212c1..2110453051 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java @@ -38,71 +38,36 @@ public class UserSessionAdapter implements UserSessionModel { return entity.getId(); } - @Override - public void setId(String id) { - entity.setId(id); - } - @Override public UserModel getUser() { return session.users().getUserById(entity.getUserId(), realm); } - @Override - public void setUser(UserModel user) { - entity.setUserId(user.getId()); - } - @Override public String getLoginUsername() { return entity.getLoginUsername(); } - @Override - public void setLoginUsername(String loginUsername) { - entity.setLoginUsername(loginUsername); - } - @Override public String getIpAddress() { return entity.getIpAddress(); } - @Override - public void setIpAddress(String ipAddress) { - entity.setIpAddress(ipAddress); - } - @Override public String getAuthMethod() { return entity.getAuthMethod(); } - @Override - public void setAuthMethod(String authMethod) { - entity.setAuthMethod(authMethod); - } - @Override public boolean isRememberMe() { return entity.isRememberMe(); } - @Override - public void setRememberMe(boolean rememberMe) { - entity.setRememberMe(rememberMe); - } - @Override public int getStarted() { return entity.getStarted(); } - @Override - public void setStarted(int started) { - entity.setStarted(started); - } - @Override public int getLastSessionRefresh() { return entity.getLastSessionRefresh(); diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java index 7ef7e774d2..27f9757b79 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java @@ -239,17 +239,6 @@ public class MemUserSessionProvider implements UserSessionProvider { return new UsernameLoginFailureAdapter(entity); } - @Override - public List getAllUserLoginFailures(RealmModel realm) { - List failures = new LinkedList(); - for (UsernameLoginFailureEntity entity : loginFailures.values()) { - if (entity.getRealm().equals(realm.getId())) { - failures.add(new UsernameLoginFailureAdapter(entity)); - } - } - return failures; - } - @Override public void onRealmRemoved(RealmModel realm) { removeUserSessions(realm); diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java index 3b8e8674c3..e2da268763 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java @@ -55,47 +55,24 @@ public class UserSessionAdapter implements UserSessionModel { return entity.getLoginUsername(); } - @Override - public void setLoginUsername(String loginUsername) { - entity.setLoginUsername(loginUsername); - } - public String getIpAddress() { return entity.getIpAddress(); } - public void setIpAddress(String ipAddress) { - entity.setIpAddress(ipAddress); - } - @Override public String getAuthMethod() { return entity.getAuthMethod(); } - @Override - public void setAuthMethod(String authMethod) { - entity.setAuthMethod(authMethod); - } - @Override public boolean isRememberMe() { return entity.isRememberMe(); } - @Override - public void setRememberMe(boolean rememberMe) { - entity.setRememberMe(rememberMe); - } - public int getStarted() { return entity.getStarted(); } - public void setStarted(int started) { - entity.setStarted(started); - } - public int getLastSessionRefresh() { return entity.getLastSessionRefresh(); } diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java index a508c3490c..9c5f6fd3bd 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java @@ -221,22 +221,6 @@ public class MongoUserSessionProvider implements UserSessionProvider { return new UsernameLoginFailureAdapter(invocationContext, userEntity); } - @Override - public List getAllUserLoginFailures(RealmModel realm) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .get(); - List failures = mongoStore.loadEntities(MongoUsernameLoginFailureEntity.class, query, invocationContext); - - List result = new LinkedList(); - if (failures == null) return result; - for (MongoUsernameLoginFailureEntity failure : failures) { - result.add(new UsernameLoginFailureAdapter(invocationContext, failure)); - } - - return result; - } - @Override public void onRealmRemoved(RealmModel realm) { removeUserSessions(realm); diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java index e47833d98a..70ba85a095 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java @@ -18,8 +18,6 @@ import java.util.List; */ public class UserSessionAdapter extends AbstractMongoAdapter implements UserSessionModel { - private static final Logger logger = Logger.getLogger(UserSessionAdapter.class); - private final MongoUserSessionProvider provider; private MongoUserSessionEntity entity; private RealmModel realm; @@ -45,78 +43,36 @@ public class UserSessionAdapter extends AbstractMongoAdapter3.0.5 2.35.0 1.4.5 + 6.0.2.Final 1.6 @@ -437,6 +438,11 @@ jboss-logging-processor ${jboss-logging-tools.version} + + org.infinispan + infinispan-core + ${infinispan.version} + diff --git a/server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 6760f41bd2..513bf8c641 100755 --- a/server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -5,6 +5,7 @@ + diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index f64fc0a86d..2457705fb1 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -82,13 +82,12 @@ public class AuthenticationManager { UserModel user = userSession.getUser(); logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId()); - - session.sessions().removeUserSession(realm, userSession); expireIdentityCookie(realm, uriInfo, connection); expireRememberMeCookie(realm, uriInfo, connection); new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), userSession); + session.sessions().removeUserSession(realm, userSession); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index bf2656a494..79bc1427d7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -142,7 +142,7 @@ public class UsersResource { if (session.users().getUserByUsername(rep.getUsername(), realm) != null) { return Flows.errors().exists("User exists with same username"); } - if (session.users().getUserByEmail(rep.getEmail(), realm) != null) { + if (rep.getEmail() != null && session.users().getUserByEmail(rep.getEmail(), realm) != null) { return Flows.errors().exists("User exists with same email"); } diff --git a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java index 785cc81004..bf9194413e 100755 --- a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java +++ b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java @@ -17,17 +17,20 @@ public class JsonConfigProvider implements Config.ConfigProvider { @Override public String getProvider(String spi) { - JsonNode n = getNode(spi, "provider"); + JsonNode n = getNode(config, spi, "provider"); return n != null ? StringPropertyReplacer.replaceProperties(n.getTextValue()) : null; } @Override public Config.Scope scope(String... path) { - return new JsonScope(getNode(path)); + return new JsonScope(getNode(config, path)); } - private JsonNode getNode(String... path) { - JsonNode n = config; + private static JsonNode getNode(JsonNode root, String... path) { + if (root == null) { + return null; + } + JsonNode n = root; for (String p : path) { n = n.get(p); if (n == null) { @@ -144,6 +147,11 @@ public class JsonConfigProvider implements Config.ConfigProvider { return n.getBooleanValue(); } } + + @Override + public Config.Scope scope(String... path) { + return new JsonScope(getNode(config, path)); + } } } diff --git a/testsuite/docker-cluster/README.md b/testsuite/docker-cluster/README.md new file mode 100644 index 0000000000..2aaa4bfc9a --- /dev/null +++ b/testsuite/docker-cluster/README.md @@ -0,0 +1,135 @@ +How to test Keycloak cluster with Docker +======================================== +Docker+Fig allows to easily setup and test the whole environment with: +* Apache HTTPD 2.4 + modcluster 1.3 as Load Balancer +* MySQL 5.6.1 as database +* Various number of Keycloak cluster nodes running on WildFly (with "demo" examples deployed) + +You don't need to setup Apache with modcluster + MySQL on your laptop as Docker will do it for you and all will run in Docker containers. + +Steps to setup +-------------- +1) Download and install [Docker](https://docs.docker.com/installation) and [Fig](http://www.fig.sh/install.html) + +2) Build Keycloak including distribution. This will be used by Docker+Fig. The point is that you can test clustering stuff from latest Keycloak master: +```shell +$ cd $KEYCLOAK_HOME +$ mvn clean install +$ cd distribution +$ mvn clean install +```` + +3) Build Docker with maven to ensure that needed data will be accessible to Docker+Fig volumes: +```shell +$ cd $KEYCLOAK_HOME/testsuite/docker-cluster +$ mvn clean install +```` + +4) Build fig and run the whole env. By default it will run Apache + MySQL + 1 Keycloak node: +```shell +$ fig build +$ fig up +```` + +First build will take long time as it need to download bunch of stuff and install into Docker container. Next builds will be much faster due to Docker cache. +After some time, WildFly server is started + +Testing +------- + +Apache is running in separate container and have 2 ports exposed locally: 10001 and 8000. Port 10001 is for modCluster - you should +be able to access Apache modCluster status page: [http://localhost:10001/mod_cluster_manager](http://localhost:10001/mod_cluster_manager) and see one node +with deployed "auth-server.war" and few other WARs (keycloak demo). + +Also you can access Keycloak admin console via loadBalancer on [http://localhost:8000/auth/admin](http://localhost:8000/auth/admin) and similarly Account mgmt. +TODO: Examples currently doesn't work and I am looking at it.. + +MySQL can be directly accessed from your machine (if you have MySQL client installed): +```shell +$ mysql -h127.0.0.1 -P33306 -uroot -pmysecretpassword +```` +Used database is "keycloak_db" + +Remote debugging +---------------- + +With command: +```shell +$ docker ps +```` + +You can see running ports. For the Keycloak node you may see output similar to this: +```shell +0.0.0.0:49153->8080/tcp, 0.0.0.0:49154->8787/tcp, 0.0.0.0:49155->9990/tcp +```` + +This means that you can directly access Keycloak (bypass loadbalancer) by going to [http://localhost:49153/auth/admin](http://localhost:49153/auth/admin) . +Also it means that debugger is mapped From Docker port 8787 to local port 49154 . So in your IDE you can connect with settings similar to: +```shell +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=49154 +```` + +Scale / more cluster nodes +-------------------------- + +Run this in separate terminal to add more (in this case 2) cluster nodes: +```shell +$ fig scale node=2 +```` + +Now it should be visible on mod_cluster_manager page that they are 2 nodes. + +Seeing logs +----------- +It's easiest to do: +```shell +$ fig logs +```` +to see output of MySql and Keycloak server consoles. + +To see Apache and debug logs of keycloak server: +```shell +$ fig run node /bin/bash +```` + +Then you're in shell inside docker container, which has some mounted volumes with apache logs and keycloak nodes. Apache logs are at: +```shell +$ cd /apachelogs/ +```` + +Keycloak nodes are at (debug logging enabled by default for "org.keycloak"): +```shell +$ cd /keycloak-docker/shared +```` + +Restart whole environment +------------------------- + +Just run: +```shell +$ fig stop +$ fig start +```` + +This will restart apache + MySQL + all nodes, but won't clear data. + +Changing configuration and clear data +------------------------------------- +Changing configuration (for example UserSession provider from 'mem' to 'jpa') is possible in +```shell +$KEYCLOAK_HOME/testsuite/docker-cluster/target/keycloak-docker-cluster/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json +```` + +then whole environment needs to be stopped, containers removed (in order to update configuration in nodes) and started again: +```shell +$ fig stop +$ fig rm +$ fig up +```` + +Rebuilding after changed sources +------------------------------- +In this case you might need to stop and remove existing containers. Then start from step 2 (Rebuild Keycloak or at least +changed jars, then rebuild distribution and testsuite/docker-cluster +(or just copy changed JAR into $KEYCLOAK_HOME/testsuite/docker-cluster/target/keycloak-docker-cluster/deployments/auth-server.war/WEB-INF/lib if it's not adapter stuff. +But 'fig rm' is safer to call anyway) diff --git a/testsuite/docker-cluster/assembly.xml b/testsuite/docker-cluster/assembly.xml new file mode 100644 index 0000000000..d7c98cf688 --- /dev/null +++ b/testsuite/docker-cluster/assembly.xml @@ -0,0 +1,30 @@ + + docker-cluster + + + dir + + false + + + + ../../examples/demo-template + examples + + **/target/*.war + **/testrealm.json + + + + target/deployments + deployments + + **/keycloak-ds.xml + + + + target/modules + modules + + + diff --git a/testsuite/docker-cluster/fig.yml b/testsuite/docker-cluster/fig.yml new file mode 100644 index 0000000000..046d73b3f7 --- /dev/null +++ b/testsuite/docker-cluster/fig.yml @@ -0,0 +1,31 @@ +httpd: + build: httpd + ports: + - "8000:80" + - "10001:10001" + volumes_from: + - mysql +mysql: + image: mysql:5.6.20 + environment: + - MYSQL_ROOT_PASSWORD=mysecretpassword + - MYSQL_DATABASE=keycloak_db + volumes: + - /keycloak-docker-shared + - /apachelogs + ports: + - "33306:3306" +node: + build: wildfly + command: /keycloak-run-node.sh + volumes: + - target/keycloak-docker-cluster:/keycloak-docker-cluster + volumes_from: + - mysql + links: + - httpd:httpd + - mysql:mysql + ports: + - "8787" + - "8080" + - "9990" diff --git a/testsuite/docker-cluster/httpd/Dockerfile b/testsuite/docker-cluster/httpd/Dockerfile new file mode 100644 index 0000000000..8a0d79a401 --- /dev/null +++ b/testsuite/docker-cluster/httpd/Dockerfile @@ -0,0 +1,16 @@ +FROM fedora:20 + +RUN cd / +RUN yum -y install wget && yum -y install unzip +RUN yum clean all + +RUN wget https://dl.dropboxusercontent.com/u/5525920/apache24-modcluster131.zip +RUN unzip -q apache24-modcluster131.zip + +ADD httpd-run /bin/httpd-run +RUN chmod u+x /bin/httpd-run + +ADD httpd.conf /opt/jboss/httpd/httpd/conf/httpd.conf + +EXPOSE 80 10001 +CMD ["/bin/httpd-run"] diff --git a/testsuite/docker-cluster/httpd/httpd-run b/testsuite/docker-cluster/httpd/httpd-run new file mode 100644 index 0000000000..26e8f70265 --- /dev/null +++ b/testsuite/docker-cluster/httpd/httpd-run @@ -0,0 +1,8 @@ +#!/bin/bash + +# Make sure we're not confused by old, incompletely-shutdown httpd +# context after restarting the container. httpd won't start correctly +# if it thinks it is already running. +rm -rf /opt/jboss/httpd/httpd/logs/httpd.pid + +exec /opt/jboss/httpd/sbin/apachectl -D FOREGROUND diff --git a/testsuite/docker-cluster/httpd/httpd.conf b/testsuite/docker-cluster/httpd/httpd.conf new file mode 100644 index 0000000000..8d3758ebdb --- /dev/null +++ b/testsuite/docker-cluster/httpd/httpd.conf @@ -0,0 +1,550 @@ +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so "logs/access_log" +# with ServerRoot set to "/usr/local/apache2" will be interpreted by the +# server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log" +# will be interpreted as '/logs/access_log'. + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to specify a local disk on the +# Mutex directive, if file-based mutexes are used. If you wish to share the +# same ServerRoot for multiple httpd daemons, you will need to change at +# least PidFile. +# +ServerRoot "/opt/jboss/httpd/httpd" + +# +# Mutex: Allows you to set the mutex mechanism and mutex file directory +# for individual mutexes, or change the global defaults +# +# Uncomment and change the directory if mutexes are file-based and the default +# mutex file directory is not on a local disk or is not appropriate for some +# other reason. +# +# Mutex default:logs + +# +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +LoadModule authn_file_module /opt/jboss/httpd/lib/httpd/modules/mod_authn_file.so +#LoadModule authn_dbm_module /opt/jboss/httpd/lib/httpd/modules/mod_authn_dbm.so +#LoadModule authn_anon_module /opt/jboss/httpd/lib/httpd/modules/mod_authn_anon.so +#LoadModule authn_dbd_module /opt/jboss/httpd/lib/httpd/modules/mod_authn_dbd.so +#LoadModule authn_socache_module /opt/jboss/httpd/lib/httpd/modules/mod_authn_socache.so +LoadModule authn_core_module /opt/jboss/httpd/lib/httpd/modules/mod_authn_core.so +LoadModule authz_host_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_host.so +LoadModule authz_groupfile_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_groupfile.so +LoadModule authz_user_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_user.so +#LoadModule authz_dbm_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_dbm.so +#LoadModule authz_owner_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_owner.so +#LoadModule authz_dbd_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_dbd.so +LoadModule authz_core_module /opt/jboss/httpd/lib/httpd/modules/mod_authz_core.so +LoadModule access_compat_module /opt/jboss/httpd/lib/httpd/modules/mod_access_compat.so +LoadModule auth_basic_module /opt/jboss/httpd/lib/httpd/modules/mod_auth_basic.so +#LoadModule auth_form_module /opt/jboss/httpd/lib/httpd/modules/mod_auth_form.so +#LoadModule auth_digest_module /opt/jboss/httpd/lib/httpd/modules/mod_auth_digest.so +#LoadModule allowmethods_module /opt/jboss/httpd/lib/httpd/modules/mod_allowmethods.so +LoadModule advertise_module /opt/jboss/httpd/lib/httpd/modules/mod_advertise.so +#LoadModule file_cache_module /opt/jboss/httpd/lib/httpd/modules/mod_file_cache.so +#LoadModule cache_module /opt/jboss/httpd/lib/httpd/modules/mod_cache.so +#LoadModule cache_disk_module /opt/jboss/httpd/lib/httpd/modules/mod_cache_disk.so +#LoadModule cache_socache_module /opt/jboss/httpd/lib/httpd/modules/mod_cache_socache.so +#LoadModule socache_shmcb_module /opt/jboss/httpd/lib/httpd/modules/mod_socache_shmcb.so +#LoadModule socache_dbm_module /opt/jboss/httpd/lib/httpd/modules/mod_socache_dbm.so +#LoadModule socache_memcache_module /opt/jboss/httpd/lib/httpd/modules/mod_socache_memcache.so +#LoadModule watchdog_module /opt/jboss/httpd/lib/httpd/modules/mod_watchdog.so +#LoadModule macro_module /opt/jboss/httpd/lib/httpd/modules/mod_macro.so +#LoadModule dbd_module /opt/jboss/httpd/lib/httpd/modules/mod_dbd.so +#LoadModule dumpio_module /opt/jboss/httpd/lib/httpd/modules/mod_dumpio.so +#LoadModule echo_module /opt/jboss/httpd/lib/httpd/modules/mod_echo.so +#LoadModule buffer_module /opt/jboss/httpd/lib/httpd/modules/mod_buffer.so +#LoadModule data_module /opt/jboss/httpd/lib/httpd/modules/mod_data.so +#LoadModule ratelimit_module /opt/jboss/httpd/lib/httpd/modules/mod_ratelimit.so +LoadModule reqtimeout_module /opt/jboss/httpd/lib/httpd/modules/mod_reqtimeout.so +#LoadModule ext_filter_module /opt/jboss/httpd/lib/httpd/modules/mod_ext_filter.so +#LoadModule request_module /opt/jboss/httpd/lib/httpd/modules/mod_request.so +#LoadModule include_module /opt/jboss/httpd/lib/httpd/modules/mod_include.so +LoadModule filter_module /opt/jboss/httpd/lib/httpd/modules/mod_filter.so +#LoadModule reflector_module /opt/jboss/httpd/lib/httpd/modules/mod_reflector.so +#LoadModule substitute_module /opt/jboss/httpd/lib/httpd/modules/mod_substitute.so +#LoadModule sed_module /opt/jboss/httpd/lib/httpd/modules/mod_sed.so +#LoadModule charset_lite_module /opt/jboss/httpd/lib/httpd/modules/mod_charset_lite.so +#LoadModule deflate_module /opt/jboss/httpd/lib/httpd/modules/mod_deflate.so +#LoadModule xml2enc_module /opt/jboss/httpd/lib/httpd/modules/mod_xml2enc.so +#LoadModule proxy_html_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_html.so +LoadModule mime_module /opt/jboss/httpd/lib/httpd/modules/mod_mime.so +LoadModule log_config_module /opt/jboss/httpd/lib/httpd/modules/mod_log_config.so +#LoadModule log_debug_module /opt/jboss/httpd/lib/httpd/modules/mod_log_debug.so +#LoadModule log_forensic_module /opt/jboss/httpd/lib/httpd/modules/mod_log_forensic.so +#LoadModule logio_module /opt/jboss/httpd/lib/httpd/modules/mod_logio.so +LoadModule env_module /opt/jboss/httpd/lib/httpd/modules/mod_env.so +#LoadModule mime_magic_module /opt/jboss/httpd/lib/httpd/modules/mod_mime_magic.so +#LoadModule expires_module /opt/jboss/httpd/lib/httpd/modules/mod_expires.so +LoadModule headers_module /opt/jboss/httpd/lib/httpd/modules/mod_headers.so +#LoadModule usertrack_module /opt/jboss/httpd/lib/httpd/modules/mod_usertrack.so +#LoadModule unique_id_module /opt/jboss/httpd/lib/httpd/modules/mod_unique_id.so +LoadModule setenvif_module /opt/jboss/httpd/lib/httpd/modules/mod_setenvif.so +LoadModule version_module /opt/jboss/httpd/lib/httpd/modules/mod_version.so +#LoadModule remoteip_module /opt/jboss/httpd/lib/httpd/modules/mod_remoteip.so +LoadModule proxy_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy.so +LoadModule proxy_connect_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_connect.so +LoadModule proxy_ftp_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_ftp.so +LoadModule proxy_http_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_http.so +LoadModule proxy_fcgi_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_fcgi.so +LoadModule proxy_scgi_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_scgi.so +#LoadModule proxy_fdpass_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_fdpass.so +LoadModule proxy_wstunnel_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_wstunnel.so +LoadModule proxy_ajp_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_ajp.so +LoadModule proxy_cluster_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_cluster.so +LoadModule proxy_express_module /opt/jboss/httpd/lib/httpd/modules/mod_proxy_express.so +#LoadModule session_module /opt/jboss/httpd/lib/httpd/modules/mod_session.so +#LoadModule session_cookie_module /opt/jboss/httpd/lib/httpd/modules/mod_session_cookie.so +#LoadModule session_dbd_module /opt/jboss/httpd/lib/httpd/modules/mod_session_dbd.so +#LoadModule slotmem_shm_module /opt/jboss/httpd/lib/httpd/modules/mod_slotmem_shm.so +#LoadModule slotmem_plain_module /opt/jboss/httpd/lib/httpd/modules/mod_slotmem_plain.so +#LoadModule ssl_module /opt/jboss/httpd/lib/httpd/modules/mod_ssl.so +#LoadModule dialup_module /opt/jboss/httpd/lib/httpd/modules/mod_dialup.so +LoadModule lbmethod_byrequests_module /opt/jboss/httpd/lib/httpd/modules/mod_lbmethod_byrequests.so +LoadModule lbmethod_bytraffic_module /opt/jboss/httpd/lib/httpd/modules/mod_lbmethod_bytraffic.so +LoadModule lbmethod_bybusyness_module /opt/jboss/httpd/lib/httpd/modules/mod_lbmethod_bybusyness.so +LoadModule lbmethod_heartbeat_module /opt/jboss/httpd/lib/httpd/modules/mod_lbmethod_heartbeat.so +LoadModule unixd_module /opt/jboss/httpd/lib/httpd/modules/mod_unixd.so +#LoadModule heartbeat_module /opt/jboss/httpd/lib/httpd/modules/mod_heartbeat.so +#LoadModule heartmonitor_module /opt/jboss/httpd/lib/httpd/modules/mod_heartmonitor.so +#LoadModule dav_module /opt/jboss/httpd/lib/httpd/modules/mod_dav.so +LoadModule status_module /opt/jboss/httpd/lib/httpd/modules/mod_status.so +LoadModule autoindex_module /opt/jboss/httpd/lib/httpd/modules/mod_autoindex.so +#LoadModule asis_module /opt/jboss/httpd/lib/httpd/modules/mod_asis.so +#LoadModule info_module /opt/jboss/httpd/lib/httpd/modules/mod_info.so +#LoadModule suexec_module /opt/jboss/httpd/lib/httpd/modules/mod_suexec.so +#LoadModule cgi_module /opt/jboss/httpd/lib/httpd/modules/mod_cgi.so +#LoadModule cgid_module /opt/jboss/httpd/lib/httpd/modules/mod_cgid.so +LoadModule cluster_slotmem_module /opt/jboss/httpd/lib/httpd/modules/mod_cluster_slotmem.so +LoadModule manager_module /opt/jboss/httpd/lib/httpd/modules/mod_manager.so +#LoadModule dav_fs_module /opt/jboss/httpd/lib/httpd/modules/mod_dav_fs.so +#LoadModule dav_lock_module /opt/jboss/httpd/lib/httpd/modules/mod_dav_lock.so +#LoadModule vhost_alias_module /opt/jboss/httpd/lib/httpd/modules/mod_vhost_alias.so +#LoadModule negotiation_module /opt/jboss/httpd/lib/httpd/modules/mod_negotiation.so +LoadModule dir_module /opt/jboss/httpd/lib/httpd/modules/mod_dir.so +#LoadModule actions_module /opt/jboss/httpd/lib/httpd/modules/mod_actions.so +#LoadModule speling_module /opt/jboss/httpd/lib/httpd/modules/mod_speling.so +#LoadModule userdir_module /opt/jboss/httpd/lib/httpd/modules/mod_userdir.so +LoadModule alias_module /opt/jboss/httpd/lib/httpd/modules/mod_alias.so +#LoadModule rewrite_module /opt/jboss/httpd/lib/httpd/modules/mod_rewrite.so + + +# +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +# +User daemon +Group daemon + + + +# 'Main' server configuration +# +# The directives in this section set up the values used by the 'main' +# server, which responds to any requests that aren't handled by a +# definition. These values also provide defaults for +# any containers you may define later in the file. +# +# All of these directives may appear inside containers, +# in which case these default settings will be overridden for the +# virtual host being defined. +# + +# +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +# +ServerAdmin you@example.com + +# +# ServerName gives the name and port that the server uses to identify itself. +# This can often be determined automatically, but we recommend you specify +# it explicitly to prevent problems during startup. +# +# If your host doesn't have a registered DNS name, enter its IP address here. +# +#ServerName www.example.com:80 + +# +# Deny access to the entirety of your server's filesystem. You must +# explicitly permit access to web content directories in other +# blocks below. +# + + AllowOverride none + Require all denied + + +# +# Note that from this point forward you must specifically allow +# particular features to be enabled - so if something's not working as +# you might expect, make sure that you have specifically enabled it +# below. +# + +# +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +DocumentRoot "/opt/jboss/httpd/htdocs/htdocs" + + # + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + # + Options Indexes FollowSymLinks + + # + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # AllowOverride FileInfo AuthConfig Limit + # + AllowOverride None + + # + # Controls who can get stuff from this server. + # + Require all granted + + +# +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# + + DirectoryIndex index.html + + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + +# +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +# Note: Actually it's custom location mounted to docker volume +ErrorLog "/apachelogs/error_log" + +# +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +# +LogLevel info + + + # + # The following directives define some format nicknames for use with + # a CustomLog directive (see below). + # + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + + # You need to enable mod_logio.c to use %I and %O + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + + # + # The location and format of the access logfile (Common Logfile Format). + # If you do not define any access logfiles within a + # container, they will be logged here. Contrariwise, if you *do* + # define per- access logfiles, transactions will be + # logged therein and *not* in this file. + # + CustomLog "/apachelogs/access_log" common + + # + # If you prefer a logfile with access, agent, and referer information + # (Combined Logfile Format) you can use the following directive. + # + #CustomLog "logs/access_log" combined + + + + # + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + # + ScriptAlias /cgi-bin/ "/opt/jboss/httpd/htdocs/cgi-bin/" + + + + + # + # ScriptSock: On threaded servers, designate the path to the UNIX + # socket used to communicate with the CGI daemon of mod_cgid. + # + #Scriptsock cgisock + + +# +# "/opt/jboss/httpd/htdocs/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. +# + + AllowOverride None + Options None + Require all granted + + + + # + # TypesConfig points to the file containing the list of mappings from + # filename extension to MIME-type. + # + TypesConfig conf/mime.types + + # + # AddType allows you to add to or override the MIME configuration + # file specified in TypesConfig for specific file types. + # + #AddType application/x-gzip .tgz + # + # AddEncoding allows you to have certain browsers uncompress + # information on the fly. Note: Not all browsers support this. + # + #AddEncoding x-compress .Z + #AddEncoding x-gzip .gz .tgz + # + # If the AddEncoding directives above are commented-out, then you + # probably should define those extensions to indicate media types: + # + AddType application/x-compress .Z + AddType application/x-gzip .gz .tgz + + # + # AddHandler allows you to map certain file extensions to "handlers": + # actions unrelated to filetype. These can be either built into the server + # or added with the Action directive (see below) + # + # To use CGI scripts outside of ScriptAliased directories: + # (You will also need to add "ExecCGI" to the "Options" directive.) + # + #AddHandler cgi-script .cgi + + # For type maps (negotiated resources): + #AddHandler type-map var + + # + # Filters allow you to process content before it is sent to the client. + # + # To parse .shtml files for server-side includes (SSI): + # (You will also need to add "Includes" to the "Options" directive.) + # + #AddType text/html .shtml + #AddOutputFilter INCLUDES .shtml + + +# +# The mod_mime_magic module allows the server to use various hints from the +# contents of the file itself to determine its type. The MIMEMagicFile +# directive tells the module where the hint definitions are located. +# +#MIMEMagicFile conf/magic + +# +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html +# + +# +# MaxRanges: Maximum number of Ranges in a request before +# returning the entire resource, or one of the special +# values 'default', 'none' or 'unlimited'. +# Default setting is to accept 200 Ranges. +#MaxRanges unlimited + +# +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall may be used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +# Defaults: EnableMMAP On, EnableSendfile Off +# +#EnableMMAP off +#EnableSendfile on + +# Supplemental configuration +# +# The configuration files in the conf/extra/ directory can be +# included to add extra features or to modify the default configuration of +# the server, or you may simply copy their contents here and change as +# necessary. + +# Server-pool management (MPM specific) +#Include conf/extra/httpd-mpm.conf + +# Multi-language error messages +#Include conf/extra/httpd-multilang-errordoc.conf + +# Fancy directory listings +#Include conf/extra/httpd-autoindex.conf + +# Language settings +#Include conf/extra/httpd-languages.conf + +# User home directories +#Include conf/extra/httpd-userdir.conf + +# Real-time info on requests and configuration +#Include conf/extra/httpd-info.conf + +# Virtual hosts +#Include conf/extra/httpd-vhosts.conf + +# Local access to the Apache HTTP Server Manual +#Include conf/extra/httpd-manual.conf + +# Distributed authoring and versioning (WebDAV) +#Include conf/extra/httpd-dav.conf + +# Various default settings +#Include conf/extra/httpd-default.conf + +# Configure mod_proxy_html to understand HTML4/XHTML1 + +Include conf/extra/proxy-html.conf + + +# Secure (SSL/TLS) connections +#Include conf/extra/httpd-ssl.conf +# +# Note: The following must must be present to support +# starting without SSL on platforms with no /dev/random equivalent +# but a statically compiled-in mod_ssl. +# + +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin + +# +# uncomment out the below to deal with user agents that deliberately +# violate open standards by misusing DNT (DNT *must* be a specific +# end-user choice) +# +# +#BrowserMatch "MSIE 10.0;" bad_DNT +# +# +#RequestHeader unset DNT env=bad_DNT +# + +# MOD_CLUSTER_ADDS +# Adjust to you hostname and subnet. + + Listen *:10001 + ManagerBalancerName mycluster + + + Require all granted + + + KeepAliveTimeout 300 + MaxKeepAliveRequests 0 + #ServerAdvertise on http://@IP@:6666 + AdvertiseFrequency 5 + #AdvertiseSecurityKey secret + #AdvertiseGroup @ADVIP@:23364 + EnableMCPMReceive + + + SetHandler mod_cluster-manager + Require all granted + + + + diff --git a/testsuite/docker-cluster/pom.xml b/testsuite/docker-cluster/pom.xml new file mode 100644 index 0000000000..d552ddf439 --- /dev/null +++ b/testsuite/docker-cluster/pom.xml @@ -0,0 +1,89 @@ + + 4.0.0 + + keycloak-testsuite-pom + org.keycloak + 1.1.0-Alpha1-SNAPSHOT + ../pom.xml + + + keycloak-testsuite-docker-cluster + pom + Keycloak Docker Cluster + + + + + + keycloak-docker-cluster + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + unpack + prepare-package + + unpack + + + + + org.keycloak + keycloak-war-deployment + ${project.version} + zip + ${project.build.directory} + + + org.keycloak + keycloak-wildfly-adapter-dist + ${project.version} + zip + ${project.build.directory} + + + + + + + + maven-assembly-plugin + 2.4 + + + assemble + package + + single + + + + assembly.xml + + + target + + + target/assembly/work + + false + + + + + + + + diff --git a/testsuite/docker-cluster/wildfly/Dockerfile b/testsuite/docker-cluster/wildfly/Dockerfile new file mode 100644 index 0000000000..3727d5bd69 --- /dev/null +++ b/testsuite/docker-cluster/wildfly/Dockerfile @@ -0,0 +1,35 @@ +FROM jboss/wildfly + +USER root +RUN yum install -y unzip && yum install -y wget && yum install -y mc && yum -y install nc +RUN yum clean all + +RUN cd /tmp +RUN wget http://search.maven.org/remotecontent?filepath=mysql/mysql-connector-java/5.1.32/mysql-connector-java-5.1.32.jar +RUN mv *.jar mysql-connector-java-5.1.32.jar + +RUN mkdir -p mysql/main && mv mysql-connector-java-5.1.32.jar mysql/main/ +ADD mysql-module.xml mysql/main/module.xml +RUN mv mysql /opt/wildfly/modules/system/layers/base/com/ + +RUN sed -i -e 's//&\n /' /opt/wildfly/standalone/configuration/standalone-ha.xml && \ +sed -i -e 's//&\n /' /opt/wildfly/standalone/configuration/standalone-ha.xml && \ +sed -i -e 's//&\n \n \n \n <\/authentication>\n <\/security-domain>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \ +sed -i -e 's//&\n \n com.mysql.jdbc.Driver<\/xa-datasource-class>\n <\/driver>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \ +sed -i -e 's/<\/periodic-rotating-file-handler>/&\n \n \n <\/logger>/' /opt/wildfly/standalone/configuration/standalone-ha.xml + +RUN sed -i -e 's//&\n \ +\n \n \ +\n \n <\/cache-container>/' /opt/wildfly/standalone/configuration/standalone-ha.xml + +RUN sed -i "s|||" /opt/wildfly/standalone/configuration/standalone-ha.xml + +RUN sed -i "s|#JAVA_OPTS=\"\$JAVA_OPTS -agentlib:jdwp=transport=dt_socket|JAVA_OPTS=\"\$JAVA_OPTS -agentlib:jdwp=transport=dt_socket|" /opt/wildfly/bin/standalone.conf + +ADD mysql-keycloak-ds.xml /opt/wildfly/standalone/deployments/ +ADD keycloak-run-node.sh /keycloak-run-node.sh +RUN chmod u+x /keycloak-run-node.sh + +EXPOSE 8787 + +CMD [ "/keycloak-run-node.sh" ] diff --git a/testsuite/docker-cluster/wildfly/keycloak-run-node.sh b/testsuite/docker-cluster/wildfly/keycloak-run-node.sh new file mode 100644 index 0000000000..b9af405439 --- /dev/null +++ b/testsuite/docker-cluster/wildfly/keycloak-run-node.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +export MYHOST=node$(echo $MYSQL_NAME | awk -F"/dockercluster[^0-9]*|\/mysql" '{print $2 }'); +echo "MYHOST is $MYHOST. MYSQL_NAME is $MYSQL_NAME"; + +function waitForPreviousNodeStart +{ + myHostNumber=$(echo $MYHOST | awk -F"node" '{ print $2 }'); + if [ $myHostNumber -eq 1 ]; then + echo "Our host is node1. No need to wait for previous server"; + else + previous=node$(($myHostNumber-1)); + echo "Waiting for host $previous to start"; + + for I in $(seq 1 10); do + cat /keycloak-docker-shared/keycloak-wildfly-$previous/standalone/log/server.log | grep "\(INFO\|ERROR\).*WildFly.*started"; + if [ 0 -eq $? ]; then + echo "Host $previous started. Going to start $MYHOST"; + return; + fi; + + echo "Host $previous not started yet. Still waiting..."; + sleep 5; + done; + + echo "Host $previous not started yet within timeout."; + fi; +} + +function waitForMySQLStart +{ + for I in $(seq 1 10); do + nc $MYSQL_PORT_3306_TCP_ADDR 3306 < /dev/null; + mysqlRunning=$(echo $?); + if [ $mysqlRunning -eq 0 ]; then + echo "MySQL is running. Starting our server"; + return; + else + echo "MySQL not yet available. Still waiting..."; + sleep 5; + fi; + done; +} + +echo "Creating keycloak-wildfly-$MYHOST"; + +cd /opt/wildfly +cp -r /keycloak-docker-cluster/modules ./ + +# Deploy keycloak +cp -r /keycloak-docker-cluster/deployments/* /opt/wildfly/standalone/deployments/ + +# Deploy examples +cd /keycloak-docker-cluster/examples +for I in $(find . | grep .war$); do cp $I /opt/wildfly/standalone/deployments/; done; + +# Deploy to volume +rm -rf /keycloak-docker-shared/keycloak-wildfly-$MYHOST +cp -r /opt/wildfly-8.1.0.Final /keycloak-docker-shared/keycloak-wildfly-$MYHOST +chmod -R 777 /keycloak-docker-shared/keycloak-wildfly-$MYHOST +echo "keycloak-wildfly-$MYHOST prepared and copyied to volume"; + + +waitForPreviousNodeStart; +waitForMySQLStart; + +echo "Running keycloak node $MYHOST. Additional arguments: $@"; +cd /keycloak-docker-shared +export JBOSS_HOME=/keycloak-docker-shared/keycloak-wildfly-$MYHOST; + +cd $JBOSS_HOME/bin/ + +./standalone.sh -c standalone-ha.xml -Djboss.node.name=$MYHOST -b `hostname -i` -Djboss.mod_cluster.jvmRoute=$MYHOST \ +-Dmysql.host=$MYSQL_PORT_3306_TCP_ADDR -Dhttpd.proxyList=$HTTPD_1_PORT_10001_TCP_ADDR:$HTTPD_PORT_10001_TCP_PORT \ +-Dkeycloak.import=/keycloak-docker-cluster/examples/testrealm.json "$@" diff --git a/testsuite/docker-cluster/wildfly/mysql-keycloak-ds.xml b/testsuite/docker-cluster/wildfly/mysql-keycloak-ds.xml new file mode 100644 index 0000000000..cd6982e592 --- /dev/null +++ b/testsuite/docker-cluster/wildfly/mysql-keycloak-ds.xml @@ -0,0 +1,11 @@ + + + + jdbc:mysql://${mysql.host}/keycloak_db + mysql + + root + mysecretpassword + + + diff --git a/testsuite/docker-cluster/wildfly/mysql-module.xml b/testsuite/docker-cluster/wildfly/mysql-module.xml new file mode 100644 index 0000000000..82490c0a41 --- /dev/null +++ b/testsuite/docker-cluster/wildfly/mysql-module.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index b6053bfe89..6941eb3364 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -183,6 +183,10 @@ + + org.infinispan + infinispan-core + org.seleniumhq.selenium selenium-java @@ -377,6 +381,26 @@ + + infinispan + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + infinispan + infinispan + infinispan + + + + + + + diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java index fc9b9fe8c0..3f0279d106 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -156,7 +156,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { } public ExpectedEvent expectRegister(String username, String email) { - UserRepresentation user = keycloak.getUser("test", username); + UserRepresentation user = username != null ? keycloak.getUser("test", username) : null; return expect(EventType.REGISTER) .user(user != null ? user.getId() : null) .detail(Details.USERNAME, username) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 3eb3daead2..3ce90ddd2b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -160,12 +160,12 @@ public class AccountTest { }); } - @Test - @Ignore - public void runit() throws Exception { - Thread.sleep(10000000); - - } +// @Test +// @Ignore +// public void runit() throws Exception { +// Thread.sleep(10000000); +// +// } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java index 9cb61f55d8..298ebd5b7b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java @@ -104,12 +104,12 @@ public class FederationProvidersIntegrationTest { @WebResource protected AccountPasswordPage changePasswordPage; - @Test - @Ignore - public void runit() throws Exception { - Thread.sleep(10000000); - - } +// @Test +// @Ignore +// public void runit() throws Exception { +// Thread.sleep(10000000); +// +// } static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) { UserModel user = session.users().addUser(realm, username); diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 07fe7f79da..8780d19241 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -29,6 +29,7 @@ performance tools performance-web +