Clustering support

This commit is contained in:
Stian Thorgersen 2014-09-17 20:57:09 +02:00
parent 6075ef221f
commit 99c73a9cc7
67 changed files with 3250 additions and 176 deletions

33
connections/infinispan/pom.xml Executable file
View file

@ -0,0 +1,33 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-connections-infinispan</artifactId>
<name>Keycloak Connections Infinispan</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,26 @@
package org.keycloak.connections.infinispan;
import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider {
private EmbeddedCacheManager cacheManager;
public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public <K, V> Cache<K, V> getCache(String name) {
return cacheManager.getCache(name);
}
@Override
public void close() {
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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();
}
}

View file

@ -0,0 +1,13 @@
package org.keycloak.connections.infinispan;
import org.infinispan.Cache;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface InfinispanConnectionProvider extends Provider {
<K, V> Cache<K, V> getCache(String name);
}

View file

@ -0,0 +1,9 @@
package org.keycloak.connections.infinispan;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface InfinispanConnectionProviderFactory extends ProviderFactory<InfinispanConnectionProvider> {
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InfinispanConnectionSpi implements Spi {
@Override
public String getName() {
return "connectionsInfinispan";
}
@Override
public Class<? extends Provider> getProviderClass() {
return InfinispanConnectionProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return InfinispanConnectionProviderFactory.class;
}
}

View file

@ -0,0 +1 @@
org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory

View file

@ -0,0 +1 @@
org.keycloak.connections.infinispan.InfinispanConnectionSpi

View file

@ -14,6 +14,7 @@
<modules>
<module>jpa</module>
<module>infinispan</module>
<module>mongo</module>
</modules>

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -26,6 +26,11 @@
<artifactId>keycloak-connections-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-infinispan</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
@ -46,6 +51,16 @@
<artifactId>keycloak-model-sessions-mongo</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-sessions-infinispan</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-invalidation-cache-infinispan</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-events-jpa</artifactId>

View file

@ -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);

View file

@ -28,7 +28,6 @@ public interface UserSessionProvider extends Provider {
UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username);
UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username);
List<UsernameLoginFailureModel> getAllUserLoginFailures(RealmModel realm);
void onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client);

View file

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-invalidation-cache-infinispan</artifactId>
<name>Keycloak Invalidation Cache Infinispan</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-invalidation-cache-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-infinispan</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFactory {
protected final ConcurrentHashMap<String, String> realmLookup = new ConcurrentHashMap<String, String>();
@Override
public CacheRealmProvider create(KeycloakSession session) {
Cache<String, Object> 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";
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String, CachedUser> 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<String, CachedUser> 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<String, CachedUser> 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<String, ConcurrentHashMap<String, String>> lookup = new ConcurrentHashMap<String, ConcurrentHashMap<String, String>>();
public void put(String realm, String key, String value) {
ConcurrentHashMap<String, String> map = lookup.get(realm);
if(map == null) {
map = new ConcurrentHashMap<String, String>();
ConcurrentHashMap<String, String> p = lookup.putIfAbsent(realm, map);
if (p != null) {
map = p;
}
}
map.put(key, value);
}
public String get(String realm, String key) {
ConcurrentHashMap<String, String> map = lookup.get(realm);
return map != null ? map.get(key) : null;
}
public void remove(String realm, String key) {
ConcurrentHashMap<String, String> map = lookup.get(realm);
if (map != null) {
map.remove(key);
if (map.isEmpty()) {
lookup.remove(realm);
}
}
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InfinispanRealmCache implements RealmCache {
protected static final Logger logger = Logger.getLogger(InfinispanRealmCache.class);
protected final Cache<String, Object> cache;
protected final ConcurrentHashMap<String, String> realmLookup;
protected volatile boolean enabled = true;
public InfinispanRealmCache(Cache<String, Object> cache, ConcurrentHashMap<String, String> 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> T get(String id, Class<T> type) {
Object o = cache.get(id);
return o != null && type.isInstance(o) ? type.cast(o) : null;
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InfinispanUserCache implements UserCache {
protected static final Logger logger = Logger.getLogger(InfinispanRealmCache.class);
protected volatile boolean enabled = true;
protected final Cache<String, CachedUser> cache;
protected final InfinispanCacheUserProviderFactory.RealmLookup usernameLookup;
protected final InfinispanCacheUserProviderFactory.RealmLookup emailLookup;
public InfinispanUserCache(Cache<String, CachedUser> 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<String, CachedUser> u : cache.entrySet()) {
if (u.getValue().getRealm().equals(realmId)) {
cache.remove(u.getKey());
}
}
}
@Override
public void clear() {
cache.clear();
}
}

View file

@ -0,0 +1 @@
org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory

View file

@ -0,0 +1 @@
org.keycloak.models.cache.infinispan.InfinispanCacheUserProviderFactory

View file

@ -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;
}

View file

@ -26,5 +26,6 @@
</build>
<modules>
<module>model-adapters</module>
<module>infinispan</module>
</modules>
</project>

View file

@ -32,5 +32,6 @@
<module>sessions-jpa</module>
<module>sessions-mem</module>
<module>sessions-mongo</module>
<module>sessions-infinispan</module>
</modules>
</project>

View file

@ -0,0 +1,41 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-model-sessions-infinispan</artifactId>
<name>Keycloak Model Sessions Infinispan</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-infinispan</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientSessionAdapter implements ClientSessionModel {
private KeycloakSession session;
private InfinispanUserSessionProvider provider;
private Cache<String, SessionEntity> cache;
private RealmModel realm;
private ClientSessionEntity entity;
public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> 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<String> getRoles() {
return entity.getRoles();
}
@Override
public void setRoles(Set<String> 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<String, String>());
}
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);
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InfinispanUserSessionProvider implements UserSessionProvider {
private final KeycloakSession session;
private final Cache<String, SessionEntity> sessionCache;
private final Cache<String, LoginFailureEntity> loginFailureCache;
private final InfinispanKeycloakTransaction tx;
public InfinispanUserSessionProvider(KeycloakSession session, Cache<String, SessionEntity> sessionCache, Cache<String, LoginFailureEntity> 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<UserSessionModel> getUserSessions(RealmModel realm, UserModel user) {
Map<String, UserSessionEntity> sessions = new MapReduceTask(sessionCache)
.mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()))
.reducedWith(new FirstResultReducer())
.execute();
return wrapUserSessions(realm, sessions.values());
}
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client) {
return getUserSessions(realm, client, -1, -1);
}
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
Map<String, Integer> map = new MapReduceTask(sessionCache)
.mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp())
.reducedWith(new LargestResultReducer())
.execute();
List<Map.Entry<String, Integer>> sessionTimestamps = new LinkedList<Map.Entry<String, Integer>>(map.entrySet());
Collections.sort(sessionTimestamps, new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> e1, Map.Entry<String, Integer> 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<UserSessionModel> userSessions = new LinkedList<UserSessionModel>();
for (Map.Entry<String, Integer> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String>());
}
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<String, String> 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<UserSessionModel> wrapUserSessions(RealmModel realm, Collection<UserSessionEntity> entities) {
List<UserSessionModel> models = new LinkedList<UserSessionModel>();
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<ClientSessionModel> wrapClientSessions(RealmModel realm, Collection<ClientSessionEntity> entities) {
List<ClientSessionModel> models = new LinkedList<ClientSessionModel>();
for (ClientSessionEntity e : entities) {
models.add(wrap(realm, e));
}
return models;
}
class InfinispanKeycloakTransaction implements KeycloakTransaction {
private boolean active;
private boolean rollback;
private Map<String, CacheTask> tasks = new HashMap<String, CacheTask>();
@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
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String, SessionEntity> cache = connections.getCache(SESSION_CACHE_NAME);
Cache<String, LoginFailureEntity> 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";
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserSessionAdapter implements UserSessionModel {
private final KeycloakSession session;
private final InfinispanUserSessionProvider provider;
private final Cache<String, SessionEntity> cache;
private final RealmModel realm;
private final UserSessionEntity entity;
public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> 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<ClientSessionModel> getClientSessions() {
if (entity.getClientSessions() != null) {
List<ClientSessionEntity> clientSessions = new LinkedList<ClientSessionEntity>();
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);
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UsernameLoginFailureAdapter implements UsernameLoginFailureModel {
private InfinispanUserSessionProvider provider;
private Cache<String, LoginFailureEntity> cache;
private LoginFailureEntity entity;
public UsernameLoginFailureAdapter(InfinispanUserSessionProvider provider, Cache<String, LoginFailureEntity> 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);
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String> roles;
private Map<String, String> 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<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public Map<String, String> getNotes() {
return notes;
}
public void setNotes(Map<String, String> notes) {
this.notes = notes;
}
}

View file

@ -0,0 +1,67 @@
package org.keycloak.models.sessions.infinispan.entities;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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;
}
}

View file

@ -0,0 +1,29 @@
package org.keycloak.models.sessions.infinispan.entities;
import java.io.Serializable;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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;
}
}

View file

@ -0,0 +1,89 @@
package org.keycloak.models.sessions.infinispan.entities;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String> 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<String> getClientSessions() {
return clientSessions;
}
public void setClientSessions(Set<String> clientSessions) {
this.clientSessions = clientSessions;
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientSessionMapper implements Mapper<String, SessionEntity, String, Object>, 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;
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FirstResultReducer implements Reducer<Object, Object>, Serializable {
@Override
public Object reduce(Object reducedKey, Iterator<Object> itr) {
return itr.next();
}
}

View file

@ -0,0 +1,24 @@
package org.keycloak.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Reducer;
import java.util.Iterator;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LargestResultReducer implements Reducer<String, Integer> {
@Override
public Integer reduce(String reducedKey, Iterator<Integer> itr) {
Integer largest = itr.next();
while (itr.hasNext()) {
Integer next = itr.next();
if (next > largest) {
largest = next;
}
}
return largest;
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class SessionMapper implements Mapper<String, SessionEntity, String, Object>, 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;
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserSessionMapper implements Mapper<String, SessionEntity, String, Object>, 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;
}
}
}

View file

@ -0,0 +1 @@
org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory

View file

@ -86,17 +86,6 @@ public class JpaUserSessionProvider implements UserSessionProvider {
return new UsernameLoginFailureAdapter(entity);
}
@Override
public List<UsernameLoginFailureModel> getAllUserLoginFailures(RealmModel realm) {
TypedQuery<UsernameLoginFailureEntity> query = em.createNamedQuery("getAllFailures", UsernameLoginFailureEntity.class);
List<UsernameLoginFailureEntity> entities = query.getResultList();
List<UsernameLoginFailureModel> models = new ArrayList<UsernameLoginFailureModel>();
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();

View file

@ -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();

View file

@ -239,17 +239,6 @@ public class MemUserSessionProvider implements UserSessionProvider {
return new UsernameLoginFailureAdapter(entity);
}
@Override
public List<UsernameLoginFailureModel> getAllUserLoginFailures(RealmModel realm) {
List<UsernameLoginFailureModel> failures = new LinkedList<UsernameLoginFailureModel>();
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);

View file

@ -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();
}

View file

@ -221,22 +221,6 @@ public class MongoUserSessionProvider implements UserSessionProvider {
return new UsernameLoginFailureAdapter(invocationContext, userEntity);
}
@Override
public List<UsernameLoginFailureModel> getAllUserLoginFailures(RealmModel realm) {
DBObject query = new QueryBuilder()
.and("realmId").is(realm.getId())
.get();
List<MongoUsernameLoginFailureEntity> failures = mongoStore.loadEntities(MongoUsernameLoginFailureEntity.class, query, invocationContext);
List<UsernameLoginFailureModel> result = new LinkedList<UsernameLoginFailureModel>();
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);

View file

@ -18,8 +18,6 @@ import java.util.List;
*/
public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEntity> 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 AbstractMongoAdapter<MongoUserSessionEnt
return entity.getId();
}
@Override
public void setId(String id) {
entity.setId(id);
updateMongoEntity();
}
@Override
public UserModel getUser() {
return keycloakSession.users().getUserById(entity.getUser(), realm);
}
@Override
public void setUser(UserModel user) {
entity.setUser(user.getId());
updateMongoEntity();
}
@Override
public String getLoginUsername() {
return entity.getLoginUsername();
}
@Override
public void setLoginUsername(String loginUsername) {
entity.setLoginUsername(loginUsername);
updateMongoEntity();
}
@Override
public String getIpAddress() {
return entity.getIpAddress();
}
@Override
public void setIpAddress(String ipAddress) {
entity.setIpAddress(ipAddress);
updateMongoEntity();
}
@Override
public String getAuthMethod() {
return entity.getAuthMethod();
}
@Override
public void setAuthMethod(String authMethod) {
entity.setAuthMethod(authMethod);
updateMongoEntity();
}
@Override
public boolean isRememberMe() {
return entity.isRememberMe();
}
@Override
public void setRememberMe(boolean rememberMe) {
entity.setRememberMe(rememberMe);
updateMongoEntity();
}
@Override
public int getStarted() {
return entity.getStarted();
}
@Override
public void setStarted(int started) {
entity.setStarted(started);
updateMongoEntity();
}
@Override
public int getLastSessionRefresh() {
return entity.getLastSessionRefresh();

View file

@ -43,6 +43,7 @@
<twitter4j.version>3.0.5</twitter4j.version>
<selenium.version>2.35.0</selenium.version>
<javax.mail.version>1.4.5</javax.mail.version>
<infinispan.version>6.0.2.Final</infinispan.version>
<!-- maven-compiler-plugin -->
<maven.compiler.target>1.6</maven.compiler.target>
@ -437,6 +438,11 @@
<artifactId>jboss-logging-processor</artifactId>
<version>${jboss-logging-tools.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<version>${infinispan.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View file

@ -5,6 +5,7 @@
<module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
<module name="org.codehaus.jackson.jackson-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.infinispan"/>
</dependencies>
<exclusions>
<module name="org.jboss.resteasy.resteasy-jackson2-provider"/>

View file

@ -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);
}

View file

@ -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");
}

View file

@ -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));
}
}
}

View file

@ -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)

View file

@ -0,0 +1,30 @@
<assembly>
<id>docker-cluster</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>../../examples/demo-template</directory>
<outputDirectory>examples</outputDirectory>
<includes>
<include>**/target/*.war</include>
<include>**/testrealm.json</include>
</includes>
</fileSet>
<fileSet>
<directory>target/deployments</directory>
<outputDirectory>deployments</outputDirectory>
<excludes>
<exclude>**/keycloak-ds.xml</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>target/modules</directory>
<outputDirectory>modules</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View file

@ -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"

View file

@ -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"]

View file

@ -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

View file

@ -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 <URL:http://httpd.apache.org/docs/2.4/> for detailed information.
# In particular, see
# <URL:http://httpd.apache.org/docs/2.4/mod/directives.html>
# 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 <VirtualHost>
# 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
<IfModule unixd_module>
#
# 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
</IfModule>
# '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
# <VirtualHost> definition. These values also provide defaults for
# any <VirtualHost> containers you may define later in the file.
#
# All of these directives may appear inside <VirtualHost> 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
# <Directory> blocks below.
#
<Directory />
AllowOverride none
Require all denied
</Directory>
#
# 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"
<Directory "/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
</Directory>
#
# DirectoryIndex: sets the file that Apache will serve if a directory
# is requested.
#
<IfModule dir_module>
DirectoryIndex index.html
</IfModule>
#
# The following lines prevent .htaccess and .htpasswd files from being
# viewed by Web clients.
#
<Files ".ht*">
Require all denied
</Files>
#
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
# container, error messages relating to that virtual host will be
# logged here. If you *do* define an error logfile for a <VirtualHost>
# 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
<IfModule log_config_module>
#
# 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
<IfModule logio_module>
# 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
</IfModule>
#
# The location and format of the access logfile (Common Logfile Format).
# If you do not define any access logfiles within a <VirtualHost>
# container, they will be logged here. Contrariwise, if you *do*
# define per-<VirtualHost> 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
</IfModule>
<IfModule alias_module>
#
# 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 <Directory> 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/"
</IfModule>
<IfModule cgid_module>
#
# ScriptSock: On threaded servers, designate the path to the UNIX
# socket used to communicate with the CGI daemon of mod_cgid.
#
#Scriptsock cgisock
</IfModule>
#
# "/opt/jboss/httpd/htdocs/cgi-bin" should be changed to whatever your ScriptAliased
# CGI directory exists, if you have that configured.
#
<Directory "/opt/jboss/httpd/htdocs/cgi-bin">
AllowOverride None
Options None
Require all granted
</Directory>
<IfModule mime_module>
#
# 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
</IfModule>
#
# 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
<IfModule proxy_html_module>
Include conf/extra/proxy-html.conf
</IfModule>
# 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.
#
<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>
#
# 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)
#
#<IfModule setenvif_module>
#BrowserMatch "MSIE 10.0;" bad_DNT
#</IfModule>
#<IfModule headers_module>
#RequestHeader unset DNT env=bad_DNT
#</IfModule>
# MOD_CLUSTER_ADDS
# Adjust to you hostname and subnet.
<IfModule manager_module>
Listen *:10001
ManagerBalancerName mycluster
<VirtualHost *:10001>
<Location />
Require all granted
</Location>
KeepAliveTimeout 300
MaxKeepAliveRequests 0
#ServerAdvertise on http://@IP@:6666
AdvertiseFrequency 5
#AdvertiseSecurityKey secret
#AdvertiseGroup @ADVIP@:23364
EnableMCPMReceive
<Location /mod_cluster_manager>
SetHandler mod_cluster-manager
Require all granted
</Location>
</VirtualHost>
</IfModule>

View file

@ -0,0 +1,89 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>keycloak-testsuite-docker-cluster</artifactId>
<packaging>pom</packaging>
<name>Keycloak Docker Cluster</name>
<description/>
<dependencies>
</dependencies>
<build>
<finalName>keycloak-docker-cluster</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<!--<version>2.7</version> -->
<executions>
<execution>
<id>unpack</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-war-deployment</artifactId>
<version>${project.version}</version>
<type>zip</type>
<outputDirectory>${project.build.directory}</outputDirectory>
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
<outputDirectory>${project.build.directory}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<id>assemble</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
<outputDirectory>
target
</outputDirectory>
<workDirectory>
target/assembly/work
</workDirectory>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -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/<extensions>/&\n <extension module="org.keycloak.keycloak-wildfly-subsystem"\/>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
sed -i -e 's/<profile>/&\n <subsystem xmlns="urn:jboss:domain:keycloak:1.0"\/>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
sed -i -e 's/<security-domains>/&\n <security-domain name="keycloak">\n <authentication>\n <login-module code="org.keycloak.adapters.jboss.KeycloakLoginModule" flag="required"\/>\n <\/authentication>\n <\/security-domain>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
sed -i -e 's/<drivers>/&\n <driver name="mysql" module="com.mysql">\n <xa-datasource-class>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 <logger category=\"org.keycloak\">\n <level name=\"DEBUG\" \/> \n <\/logger>/' /opt/wildfly/standalone/configuration/standalone-ha.xml
RUN sed -i -e 's/<subsystem xmlns=\"urn:jboss:domain:infinispan:2\.0\">/&\n <cache-container name=\"keycloak\" jndi-name=\"infinispan\/Keycloak\" start=\"EAGER\"> \
\n <transport lock-timeout=\"60000\"\/>\n <distributed-cache name=\"sessions\" mode=\"SYNC\" owners=\"2\" segments=\"60\"\/> \
\n <invalidation-cache name=\"realms\" mode=\"SYNC\"\/>\n <\/cache-container>/' /opt/wildfly/standalone/configuration/standalone-ha.xml
RUN sed -i "s|<mod-cluster-config .*>|<mod-cluster-config advertise-socket=\"modcluster\" proxy-list=\"\$\{httpd.proxyList\}\" proxy-url=\"\/\" balancer=\"mycluster\" advertise=\"false\" connector=\"ajp\" sticky-session=\"true\">|" /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" ]

View file

@ -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 "$@"

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<datasources xmlns="http://www.jboss.org/ironjacamar/schema">
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
<connection-url>jdbc:mysql://${mysql.host}/keycloak_db</connection-url>
<driver>mysql</driver>
<security>
<user-name>root</user-name>
<password>mysecretpassword</password>
</security>
</datasource>
</datasources>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ JBoss, Home of Professional Open Source.
~ Copyright 2011, Red Hat, Inc., and individual contributors
~ as indicated by the @author tags. See the copyright.txt file in the
~ distribution for a full listing of individual contributors.
~
~ This is free software; you can redistribute it and/or modify it
~ under the terms of the GNU Lesser General Public License as
~ published by the Free Software Foundation; either version 2.1 of
~ the License, or (at your option) any later version.
~
~ This software is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
~ Lesser General Public License for more details.
~
~ You should have received a copy of the GNU Lesser General Public
~ License along with this software; if not, write to the Free
~ Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->
<module xmlns="urn:jboss:module:1.0" name="com.mysql">
<resources>
<resource-root path="mysql-connector-java-5.1.32.jar"/>
</resources>
<dependencies>
<module name="javax.api" />
<module name="javax.transaction.api" />
</dependencies>
</module>

View file

@ -183,6 +183,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
@ -377,6 +381,26 @@
</profile>
<profile>
<id>infinispan</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<keycloak.realm.cache.provider>infinispan</keycloak.realm.cache.provider>
<keycloak.user.cache.provider>infinispan</keycloak.user.cache.provider>
<keycloak.userSessions.provider>infinispan</keycloak.userSessions.provider>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<!-- MySQL -->
<profile>
<activation>

View file

@ -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)

View file

@ -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);
//
// }

View file

@ -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);

View file

@ -29,6 +29,7 @@
<module>performance</module>
<module>tools</module>
<module>performance-web</module>
<!--<module>docker-cluster</module>-->
</modules>
</project>