KEYCLOAK-16755 ClearExpiredUserSessions optimization. Rely on infinispan expiration rather than Keycloak own background task.
This commit is contained in:
parent
6da396821a
commit
f4b5942c6c
39 changed files with 1046 additions and 405 deletions
|
@ -65,7 +65,7 @@ public class Time {
|
||||||
* @param time Time in seconds since the epoch
|
* @param time Time in seconds since the epoch
|
||||||
* @return Time in milliseconds
|
* @return Time in milliseconds
|
||||||
*/
|
*/
|
||||||
public static long toMillis(int time) {
|
public static long toMillis(long time) {
|
||||||
return time * 1000L;
|
return time * 1000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,6 @@ package org.keycloak.cluster.infinispan;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||||
import org.infinispan.commons.marshall.Externalizer;
|
|
||||||
import org.infinispan.commons.marshall.MarshallUtil;
|
|
||||||
import org.infinispan.commons.marshall.SerializeWith;
|
|
||||||
import org.infinispan.manager.EmbeddedCacheManager;
|
import org.infinispan.manager.EmbeddedCacheManager;
|
||||||
import org.infinispan.notifications.Listener;
|
import org.infinispan.notifications.Listener;
|
||||||
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
|
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
|
||||||
|
@ -39,26 +36,16 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.connections.infinispan.TopologyInfo;
|
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.RootAuthenticationSessionPredicate;
|
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInput;
|
|
||||||
import java.io.ObjectOutput;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,7 +136,8 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
|
||||||
try {
|
try {
|
||||||
V result;
|
V result;
|
||||||
if (taskTimeoutInSeconds > 0) {
|
if (taskTimeoutInSeconds > 0) {
|
||||||
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value, taskTimeoutInSeconds, TimeUnit.SECONDS);
|
long lifespanMs = InfinispanUtil.toHotrodTimeMs(crossDCAwareCacheFactory.getCache(), Time.toMillis(taskTimeoutInSeconds));
|
||||||
|
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value, lifespanMs, TimeUnit.MILLISECONDS);
|
||||||
} else {
|
} else {
|
||||||
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value);
|
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,11 +185,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
|
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
|
||||||
configureTransport(gcb, topologyInfo.getMyNodeName(), topologyInfo.getMySiteName(), jgroupsUdpMcastAddr);
|
configureTransport(gcb, topologyInfo.getMyNodeName(), topologyInfo.getMySiteName(), jgroupsUdpMcastAddr);
|
||||||
gcb.jmx()
|
gcb.jmx()
|
||||||
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + topologyInfo.getMyNodeName());
|
.domain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + topologyInfo.getMyNodeName()).enable();
|
||||||
|
} else {
|
||||||
|
gcb.jmx().domain(InfinispanConnectionProvider.JMX_DOMAIN).enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
gcb.jmx().domain(InfinispanConnectionProvider.JMX_DOMAIN).enable();
|
|
||||||
|
|
||||||
// For Infinispan 10, we go with the JBoss marshalling.
|
// For Infinispan 10, we go with the JBoss marshalling.
|
||||||
// TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream.
|
// TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream.
|
||||||
// See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details
|
// See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details
|
||||||
|
|
|
@ -47,14 +47,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
||||||
private final InfinispanUserSessionProvider provider;
|
private final InfinispanUserSessionProvider provider;
|
||||||
private AuthenticatedClientSessionEntity entity;
|
private AuthenticatedClientSessionEntity entity;
|
||||||
private final ClientModel client;
|
private final ClientModel client;
|
||||||
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
|
|
||||||
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
|
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
|
||||||
private UserSessionModel userSession;
|
private UserSessionModel userSession;
|
||||||
private boolean offline;
|
private boolean offline;
|
||||||
|
|
||||||
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
|
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
|
||||||
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
|
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
|
||||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
|
|
||||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
|
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
|
||||||
if (userSession == null) {
|
if (userSession == null) {
|
||||||
throw new NullPointerException("userSession must not be null");
|
throw new NullPointerException("userSession must not be null");
|
||||||
|
@ -65,15 +63,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.userSession = userSession;
|
this.userSession = userSession;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.userSessionUpdateTx = userSessionUpdateTx;
|
|
||||||
this.clientSessionUpdateTx = clientSessionUpdateTx;
|
this.clientSessionUpdateTx = clientSessionUpdateTx;
|
||||||
this.offline = offline;
|
this.offline = offline;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update(UserSessionUpdateTask task) {
|
|
||||||
userSessionUpdateTx.addTask(userSession.getId(), task);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update(ClientSessionUpdateTask task) {
|
private void update(ClientSessionUpdateTask task) {
|
||||||
clientSessionUpdateTx.addTask(entity.getId(), task);
|
clientSessionUpdateTx.addTask(entity.getId(), task);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
@ -39,7 +40,6 @@ import org.keycloak.models.utils.RealmInfoUtil;
|
||||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.infinispan.AdvancedCache;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -80,7 +80,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
entity.setRealmId(realm.getId());
|
entity.setRealmId(realm.getId());
|
||||||
entity.setTimestamp(Time.currentTime());
|
entity.setTimestamp(Time.currentTime());
|
||||||
|
|
||||||
tx.put(cache, id, entity);
|
int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
||||||
|
tx.put(cache, id, entity, expirationSeconds, TimeUnit.SECONDS);
|
||||||
|
|
||||||
return wrap(realm, entity);
|
return wrap(realm, entity);
|
||||||
}
|
}
|
||||||
|
@ -94,28 +95,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(String authSessionId) {
|
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(String authSessionId) {
|
||||||
// Chance created in this transaction
|
// Chance created in this transaction
|
||||||
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
|
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
|
||||||
|
|
||||||
if (entity == null) {
|
|
||||||
entity = cache.get(authSessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllExpired() {
|
||||||
|
// Rely on expiration of cache entries provided by infinispan. Nothing needed here
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpired(RealmModel realm) {
|
public void removeExpired(RealmModel realm) {
|
||||||
log.debugf("Removing expired sessions");
|
// Rely on expiration of cache entries provided by infinispan. Nothing needed here
|
||||||
|
|
||||||
int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
|
||||||
|
|
||||||
final AdvancedCache<String, RootAuthenticationSessionEntity> localCache = CacheDecorators.localCache(cache);
|
|
||||||
int localCacheSizePre = localCache.size();
|
|
||||||
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
|
||||||
localCache.entrySet()
|
|
||||||
.removeIf(RootAuthenticationSessionPredicate.create(realm.getId()).expired(expired));
|
|
||||||
|
|
||||||
log.debugf("Removed %d expired authentication sessions for realm '%s'", localCache.size() - localCacheSizePre, realm.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -25,9 +25,11 @@ import java.util.function.Supplier;
|
||||||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||||
import org.infinispan.commons.api.BasicCache;
|
import org.infinispan.commons.api.BasicCache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.CodeToTokenStoreProvider;
|
import org.keycloak.models.CodeToTokenStoreProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -51,7 +53,8 @@ public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvi
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
|
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
|
||||||
cache.put(codeId, tokenValue, lifespanSeconds, TimeUnit.SECONDS);
|
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
|
||||||
|
cache.put(codeId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
|
||||||
} catch (HotRodClientException re) {
|
} catch (HotRodClientException re) {
|
||||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
|
|
|
@ -112,7 +112,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("CacheTaskWithValue: Operation 'put' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit.toString());
|
return String.format("CacheTaskWithValue: Operation 'put' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public <K, V> void replace(Cache<K, V> cache, K key, V value) {
|
public <K, V> void replace(Cache<K, V> cache, K key, V value, long lifespan, TimeUnit lifespanUnit) {
|
||||||
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
|
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
|
||||||
|
|
||||||
Object taskKey = getTaskKey(cache, key);
|
Object taskKey = getTaskKey(cache, key);
|
||||||
|
@ -155,12 +155,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
|
tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
decorateCache(cache).replace(key, value);
|
decorateCache(cache).replace(key, value, lifespan, lifespanUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("CacheTaskWithValue: Operation 'replace' for key %s", key);
|
return String.format("CacheTaskWithValue: Operation 'replace' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -208,7 +208,6 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
if (current instanceof CacheTaskWithValue) {
|
if (current instanceof CacheTaskWithValue) {
|
||||||
return ((CacheTaskWithValue<V>) current).getValue();
|
return ((CacheTaskWithValue<V>) current).getValue();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should we have per-transaction cache for lookups?
|
// Should we have per-transaction cache for lookups?
|
||||||
|
|
|
@ -23,9 +23,11 @@ import java.util.function.Supplier;
|
||||||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||||
import org.infinispan.commons.api.BasicCache;
|
import org.infinispan.commons.api.BasicCache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.SingleUseTokenStoreProvider;
|
import org.keycloak.models.SingleUseTokenStoreProvider;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Check if Boolean can be used as single-use cache argument instead of ActionTokenValueEntity. With respect to other single-use cache usecases like "Revoke Refresh Token" .
|
* TODO: Check if Boolean can be used as single-use cache argument instead of ActionTokenValueEntity. With respect to other single-use cache usecases like "Revoke Refresh Token" .
|
||||||
|
@ -55,7 +57,8 @@ public class InfinispanSingleUseTokenStoreProvider implements SingleUseTokenStor
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
|
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
|
||||||
ActionTokenValueEntity existing = cache.putIfAbsent(tokenId, tokenValue, lifespanInSeconds, TimeUnit.SECONDS);
|
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanInSeconds));
|
||||||
|
ActionTokenValueEntity existing = cache.putIfAbsent(tokenId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
|
||||||
return existing == null;
|
return existing == null;
|
||||||
} catch (HotRodClientException re) {
|
} catch (HotRodClientException re) {
|
||||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||||
|
|
|
@ -20,16 +20,17 @@ package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||||
import org.infinispan.commons.api.BasicCache;
|
import org.infinispan.commons.api.BasicCache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.TokenRevocationStoreProvider;
|
import org.keycloak.models.TokenRevocationStoreProvider;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -57,7 +58,8 @@ public class InfinispanTokenRevocationStoreProvider implements TokenRevocationSt
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
|
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
|
||||||
cache.put(tokenId, tokenValue, lifespanSeconds + 1, TimeUnit.SECONDS);
|
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds + 1));
|
||||||
|
cache.put(tokenId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
|
||||||
} catch (HotRodClientException re) {
|
} catch (HotRodClientException re) {
|
||||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
|
|
|
@ -53,7 +53,6 @@ import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
|
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
|
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
|
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.AuthenticatedClientSessionPredicate;
|
|
||||||
import org.keycloak.models.sessions.infinispan.stream.Comparators;
|
import org.keycloak.models.sessions.infinispan.stream.Comparators;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
|
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
|
||||||
|
@ -62,7 +61,7 @@ import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
|
||||||
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
|
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -132,12 +131,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
this.offlineClientSessionCache = offlineClientSessionCache;
|
this.offlineClientSessionCache = offlineClientSessionCache;
|
||||||
this.loginFailureCache = loginFailureCache;
|
this.loginFailureCache = loginFailureCache;
|
||||||
|
|
||||||
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker);
|
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
|
||||||
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker);
|
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||||
this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker);
|
this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||||
this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker);
|
this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
|
||||||
|
|
||||||
this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker);
|
this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
|
||||||
|
|
||||||
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
||||||
|
|
||||||
|
@ -192,7 +191,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
|
|
||||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
|
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
|
||||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
|
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
|
||||||
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, false);
|
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, clientSessionUpdateTx, false);
|
||||||
|
|
||||||
// For now, the clientSession is considered transient in case that userSession was transient
|
// For now, the clientSession is considered transient in case that userSession was transient
|
||||||
UserSessionModel.SessionPersistenceState persistenceState = (userSession instanceof UserSessionAdapter && ((UserSessionAdapter) userSession).getPersistenceState() != null) ?
|
UserSessionModel.SessionPersistenceState persistenceState = (userSession instanceof UserSessionAdapter && ((UserSessionAdapter) userSession).getPersistenceState() != null) ?
|
||||||
|
@ -463,146 +462,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeAllExpired() {
|
||||||
|
// Rely on expiration of cache entries provided by infinispan. Just expire entries from persister is needed
|
||||||
|
// TODO: Avoid iteration over all realms here (Details in the KEYCLOAK-16802)
|
||||||
|
session.realms().getRealmsStream().forEach(this::removeExpired);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpired(RealmModel realm) {
|
public void removeExpired(RealmModel realm) {
|
||||||
log.debugf("Removing expired sessions");
|
// Rely on expiration of cache entries provided by infinispan. Nothing needed here besides calling persister
|
||||||
removeExpiredUserSessions(realm);
|
|
||||||
removeExpiredOfflineUserSessions(realm);
|
|
||||||
session.getProvider(UserSessionPersisterProvider.class).removeExpired(realm);
|
session.getProvider(UserSessionPersisterProvider.class).removeExpired(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeExpiredUserSessions(RealmModel realm) {
|
|
||||||
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
|
||||||
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
|
||||||
int expiredRememberMe = Time.currentTime() - (realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
|
|
||||||
int expiredRefreshRememberMe = Time.currentTime() - (realm.getSsoSessionIdleTimeoutRememberMe() > 0 ? realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout()) -
|
|
||||||
SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
|
||||||
|
|
||||||
FuturesHelper futures = new FuturesHelper();
|
|
||||||
|
|
||||||
// Each cluster node cleanups just local sessions, which are those owned by itself (+ few more taking l1 cache into account)
|
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache);
|
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
|
|
||||||
|
|
||||||
final AtomicInteger userSessionsSize = new AtomicInteger();
|
|
||||||
final AtomicInteger clientSessionsSize = new AtomicInteger();
|
|
||||||
|
|
||||||
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
|
|
||||||
localCacheStoreIgnore
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh, expiredRememberMe, expiredRefreshRememberMe))
|
|
||||||
.map(Mappers.userSessionEntity())
|
|
||||||
.forEach(new Consumer<UserSessionEntity>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(UserSessionEntity userSessionEntity) {
|
|
||||||
userSessionsSize.incrementAndGet();
|
|
||||||
|
|
||||||
Future future = sessionCache.removeAsync(userSessionEntity.getId());
|
|
||||||
futures.addTask(future);
|
|
||||||
|
|
||||||
userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
|
|
||||||
clientSessionsSize.incrementAndGet();
|
|
||||||
Future f = clientSessionCache.removeAsync(clientSessionId);
|
|
||||||
futures.addTask(f);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Removing detached clientSessions. Ignore remoteStore for stream iteration. But we will invoke remoteStore for clientSession removal propagate
|
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(clientSessionCache);
|
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localClientSessionCache);
|
|
||||||
|
|
||||||
localClientSessionCacheStoreIgnore
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(AuthenticatedClientSessionPredicate.create(realm.getId()).expired(Math.min(expired, expiredRememberMe)))
|
|
||||||
.map(Mappers.clientSessionEntity())
|
|
||||||
.forEach(new Consumer<AuthenticatedClientSessionEntity>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(AuthenticatedClientSessionEntity clientSessionEntity) {
|
|
||||||
clientSessionsSize.incrementAndGet();
|
|
||||||
|
|
||||||
Future future = clientSessionCache.removeAsync(clientSessionEntity.getId());
|
|
||||||
futures.addTask(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
futures.waitForAllToFinish();
|
|
||||||
|
|
||||||
log.debugf("Removed %d expired user sessions and %d expired client sessions for realm '%s'", userSessionsSize.get(),
|
|
||||||
clientSessionsSize.get(), realm.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeExpiredOfflineUserSessions(RealmModel realm) {
|
|
||||||
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
|
||||||
|
|
||||||
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(offlineSessionCache);
|
|
||||||
|
|
||||||
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
|
|
||||||
|
|
||||||
FuturesHelper futures = new FuturesHelper();
|
|
||||||
|
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
|
|
||||||
|
|
||||||
final AtomicInteger userSessionsSize = new AtomicInteger();
|
|
||||||
final AtomicInteger clientSessionsSize = new AtomicInteger();
|
|
||||||
|
|
||||||
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
|
|
||||||
localCacheStoreIgnore
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(predicate)
|
|
||||||
.map(Mappers.userSessionEntity())
|
|
||||||
.forEach(new Consumer<UserSessionEntity>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(UserSessionEntity userSessionEntity) {
|
|
||||||
userSessionsSize.incrementAndGet();
|
|
||||||
|
|
||||||
Future future = offlineSessionCache.removeAsync(userSessionEntity.getId());
|
|
||||||
futures.addTask(future);
|
|
||||||
userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
|
|
||||||
clientSessionsSize.incrementAndGet();
|
|
||||||
Future f = offlineClientSessionCache.removeAsync(clientSessionId);
|
|
||||||
futures.addTask(f);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Removing detached clientSessions. Ignore remoteStore for stream iteration. But we will invoke remoteStore for clientSession removal propagate
|
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache);
|
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localClientSessionCache);
|
|
||||||
|
|
||||||
localClientSessionCacheStoreIgnore
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(AuthenticatedClientSessionPredicate.create(realm.getId()).expired(expiredOffline))
|
|
||||||
.map(Mappers.clientSessionEntity())
|
|
||||||
.forEach(new Consumer<AuthenticatedClientSessionEntity>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(AuthenticatedClientSessionEntity clientSessionEntity) {
|
|
||||||
clientSessionsSize.incrementAndGet();
|
|
||||||
|
|
||||||
Future future = offlineClientSessionCache.removeAsync(clientSessionEntity.getId());
|
|
||||||
futures.addTask(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
futures.waitForAllToFinish();
|
|
||||||
|
|
||||||
log.debugf("Removed %d expired offline user sessions and %d expired offline client sessions for realm '%s'",
|
|
||||||
userSessionsSize.get(), clientSessionsSize.get(), realm.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeUserSessions(RealmModel realm) {
|
public void removeUserSessions(RealmModel realm) {
|
||||||
// Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions.
|
// Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions.
|
||||||
|
@ -794,7 +666,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
|
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
|
||||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
|
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
|
||||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
|
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
|
||||||
return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, offline) : null;
|
return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, clientSessionUpdateTx, offline) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
|
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
|
||||||
|
@ -1041,7 +913,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
|
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
|
||||||
userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
|
userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
|
||||||
|
|
||||||
return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx, offline);
|
return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, offline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.common.util.Environment;
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -56,6 +57,7 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionLis
|
||||||
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
|
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
|
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.PostMigrationEvent;
|
import org.keycloak.models.utils.PostMigrationEvent;
|
||||||
import org.keycloak.models.utils.ResetTimeOffsetEvent;
|
import org.keycloak.models.utils.ResetTimeOffsetEvent;
|
||||||
|
@ -65,6 +67,7 @@ import org.keycloak.provider.ProviderEventListener;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
|
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
|
||||||
|
|
||||||
|
@ -245,47 +248,48 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||||
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
|
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
|
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
|
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
|
||||||
boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
|
RemoteCache sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
|
||||||
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
|
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
|
||||||
return realm.getSsoSessionMaxLifespan() * 1000;
|
return Time.toMillis(realm.getSsoSessionMaxLifespan());
|
||||||
});
|
}, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
|
||||||
|
|
||||||
if (sessionsRemoteCache) {
|
if (sessionsRemoteCache != null) {
|
||||||
lastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false);
|
lastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
|
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
|
||||||
checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
|
checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
|
||||||
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
|
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
|
||||||
return realm.getSsoSessionMaxLifespan() * 1000;
|
return Time.toMillis(realm.getSsoSessionMaxLifespan());
|
||||||
});
|
}, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||||
|
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
|
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
|
||||||
boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
|
RemoteCache offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
|
||||||
return realm.getOfflineSessionIdleTimeout() * 1000;
|
return Time.toMillis(realm.getOfflineSessionIdleTimeout());
|
||||||
});
|
}, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||||
|
|
||||||
if (offlineSessionsRemoteCache) {
|
if (offlineSessionsRemoteCache != null) {
|
||||||
offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
|
offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
|
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
|
||||||
checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> {
|
checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> {
|
||||||
return realm.getOfflineSessionIdleTimeout() * 1000;
|
return Time.toMillis(realm.getOfflineSessionIdleTimeout());
|
||||||
});
|
}, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
|
||||||
|
|
||||||
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
|
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
|
||||||
checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
|
checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
|
||||||
return realm.getMaxDeltaTimeSeconds() * 1000;
|
return Time.toMillis(realm.getMaxDeltaTimeSeconds());
|
||||||
});
|
}, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <K, V extends SessionEntity> boolean checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) {
|
private <K, V extends SessionEntity> RemoteCache checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader,
|
||||||
|
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||||
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
|
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
|
||||||
|
|
||||||
if (remoteStores.isEmpty()) {
|
if (remoteStores.isEmpty()) {
|
||||||
log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
|
log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
|
||||||
return false;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
log.infof("Remote store configured for cache '%s'", ispnCache.getName());
|
log.infof("Remote store configured for cache '%s'", ispnCache.getName());
|
||||||
|
|
||||||
|
@ -297,9 +301,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||||
|
|
||||||
remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);
|
remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);
|
||||||
|
|
||||||
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache);
|
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
|
||||||
remoteCache.addClientListener(hotrodListener);
|
remoteCache.addClientListener(hotrodListener);
|
||||||
return true;
|
return remoteCache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
|
@ -27,6 +28,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||||
|
import org.keycloak.models.utils.RealmInfoUtil;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
|
@ -52,7 +54,8 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
provider.tx.replace(cache, entity.getId(), entity);
|
int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
||||||
|
provider.tx.replace(cache, entity.getId(), entity, expirationSeconds, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.changes;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.context.Flag;
|
import org.infinispan.context.Flag;
|
||||||
|
@ -47,11 +48,17 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
|
|
||||||
private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
|
private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
|
||||||
|
|
||||||
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) {
|
private final BiFunction<RealmModel, V, Long> lifespanMsLoader;
|
||||||
|
private final BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
|
||||||
|
|
||||||
|
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker,
|
||||||
|
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||||
this.kcSession = kcSession;
|
this.kcSession = kcSession;
|
||||||
this.cacheName = cache.getName();
|
this.cacheName = cache.getName();
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.remoteCacheInvoker = remoteCacheInvoker;
|
this.remoteCacheInvoker = remoteCacheInvoker;
|
||||||
|
this.lifespanMsLoader = lifespanMsLoader;
|
||||||
|
this.maxIdleTimeMsLoader = maxIdleTimeMsLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,7 +162,10 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
|
|
||||||
RealmModel realm = sessionUpdates.getRealm();
|
RealmModel realm = sessionUpdates.getRealm();
|
||||||
|
|
||||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
|
long lifespanMs = lifespanMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||||
|
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||||
|
|
||||||
|
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, lifespanMs, maxIdleTimeMs);
|
||||||
|
|
||||||
if (merged != null) {
|
if (merged != null) {
|
||||||
// Now run the operation in our cluster
|
// Now run the operation in our cluster
|
||||||
|
@ -185,21 +195,25 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
case ADD:
|
case ADD:
|
||||||
CacheDecorators.skipCacheStore(cache)
|
CacheDecorators.skipCacheStore(cache)
|
||||||
.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
|
.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
|
||||||
.put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS);
|
.put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
logger.tracef("Added entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs());
|
||||||
break;
|
break;
|
||||||
case ADD_IF_ABSENT:
|
case ADD_IF_ABSENT:
|
||||||
SessionEntityWrapper<V> existing = CacheDecorators.skipCacheStore(cache).putIfAbsent(key, sessionWrapper);
|
SessionEntityWrapper<V> existing = CacheDecorators.skipCacheStore(cache).putIfAbsent(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
logger.debugf("Existing entity in cache for key: %s . Will update it", key);
|
logger.debugf("Existing entity in cache for key: %s . Will update it", key);
|
||||||
|
|
||||||
// Apply updates on the existing entity and replace it
|
// Apply updates on the existing entity and replace it
|
||||||
task.runUpdate(existing.getEntity());
|
task.runUpdate(existing.getEntity());
|
||||||
|
|
||||||
replace(key, task, existing);
|
replace(key, task, existing, task.getLifespanMs(), task.getMaxIdleTimeMs());
|
||||||
|
} else {
|
||||||
|
logger.tracef("Add_if_absent successfully called for entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case REPLACE:
|
case REPLACE:
|
||||||
replace(key, task, sessionWrapper);
|
replace(key, task, sessionWrapper, task.getLifespanMs(), task.getMaxIdleTimeMs());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported state " + operation);
|
throw new IllegalStateException("Unsupported state " + operation);
|
||||||
|
@ -208,7 +222,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity) {
|
private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity, long lifespanMs, long maxIdleTimeMs) {
|
||||||
boolean replaced = false;
|
boolean replaced = false;
|
||||||
int iteration = 0;
|
int iteration = 0;
|
||||||
V session = oldVersionEntity.getEntity();
|
V session = oldVersionEntity.getEntity();
|
||||||
|
@ -219,7 +233,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
|
SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
|
||||||
|
|
||||||
// Atomic cluster-aware replace
|
// Atomic cluster-aware replace
|
||||||
replaced = CacheDecorators.skipCacheStore(cache).replace(key, oldVersionEntity, newVersionEntity);
|
replaced = CacheDecorators.skipCacheStore(cache).replace(key, oldVersionEntity, newVersionEntity, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
// Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again
|
// Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again
|
||||||
if (!replaced) {
|
if (!replaced) {
|
||||||
|
@ -239,7 +253,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
task.runUpdate(session);
|
task.runUpdate(session);
|
||||||
} else {
|
} else {
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion());
|
logger.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s, Lifespan: %d ms, MaxIdle: %d ms", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion(), task.getLifespanMs(), task.getMaxIdleTimeMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,21 +20,29 @@ package org.keycloak.models.sessions.infinispan.changes;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
|
public class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
|
||||||
|
|
||||||
private List<SessionUpdateTask<S>> childUpdates = new LinkedList<>();
|
private static final Logger logger = Logger.getLogger(MergedUpdate.class);
|
||||||
|
|
||||||
|
private final List<SessionUpdateTask<S>> childUpdates = new LinkedList<>();
|
||||||
private CacheOperation operation;
|
private CacheOperation operation;
|
||||||
private CrossDCMessageStatus crossDCMessageStatus;
|
private CrossDCMessageStatus crossDCMessageStatus;
|
||||||
|
private final long lifespanMs;
|
||||||
|
private final long maxIdleTimeMs;
|
||||||
|
|
||||||
|
|
||||||
private MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
|
private MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus, long lifespanMs, long maxIdleTimeMs) {
|
||||||
this.operation = operation;
|
this.operation = operation;
|
||||||
this.crossDCMessageStatus = crossDCMessageStatus;
|
this.crossDCMessageStatus = crossDCMessageStatus;
|
||||||
|
this.lifespanMs = lifespanMs;
|
||||||
|
this.maxIdleTimeMs = maxIdleTimeMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -54,8 +62,16 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
|
||||||
return crossDCMessageStatus;
|
return crossDCMessageStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLifespanMs() {
|
||||||
|
return lifespanMs;
|
||||||
|
}
|
||||||
|
|
||||||
public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<SessionUpdateTask<S>> childUpdates, SessionEntityWrapper<S> sessionWrapper) {
|
public long getMaxIdleTimeMs() {
|
||||||
|
return maxIdleTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<SessionUpdateTask<S>> childUpdates, SessionEntityWrapper<S> sessionWrapper, long lifespanMs, long maxIdleTimeMs) {
|
||||||
if (childUpdates == null || childUpdates.isEmpty()) {
|
if (childUpdates == null || childUpdates.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -64,14 +80,21 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
|
||||||
S session = sessionWrapper.getEntity();
|
S session = sessionWrapper.getEntity();
|
||||||
for (SessionUpdateTask<S> child : childUpdates) {
|
for (SessionUpdateTask<S> child : childUpdates) {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper));
|
CacheOperation operation = child.getOperation(session);
|
||||||
|
|
||||||
|
if (lifespanMs == SessionTimeouts.ENTRY_EXPIRED_FLAG || maxIdleTimeMs == SessionTimeouts.ENTRY_EXPIRED_FLAG) {
|
||||||
|
operation = CacheOperation.REMOVE;
|
||||||
|
logger.tracef("Entry '%s' is expired. Will remove it from the cache", sessionWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new MergedUpdate<>(operation, child.getCrossDCMessageStatus(sessionWrapper), lifespanMs, maxIdleTimeMs);
|
||||||
result.childUpdates.add(child);
|
result.childUpdates.add(child);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Merge the operations. REMOVE is special case as other operations are not needed then.
|
// Merge the operations. REMOVE is special case as other operations are not needed then.
|
||||||
CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session);
|
CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session);
|
||||||
if (mergedOp == CacheOperation.REMOVE) {
|
if (mergedOp == CacheOperation.REMOVE) {
|
||||||
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper));
|
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper), lifespanMs, maxIdleTimeMs);
|
||||||
result.childUpdates.add(child);
|
result.childUpdates.add(child);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,11 +30,6 @@ public interface SessionUpdateTask<S extends SessionEntity> {
|
||||||
|
|
||||||
CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper);
|
CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper);
|
||||||
|
|
||||||
default long getLifespanMs() {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
enum CacheOperation {
|
enum CacheOperation {
|
||||||
|
|
||||||
ADD,
|
ADD,
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.connections.infinispan.TopologyInfo;
|
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.sessions.infinispan.changes.MergedUpdate;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||||
|
@ -57,7 +58,7 @@ public class RemoteCacheInvoker {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public <K, V extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
|
public <K, V extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, K key, MergedUpdate<V> task, SessionEntityWrapper<V> sessionWrapper) {
|
||||||
RemoteCacheContext context = remoteCaches.get(cacheName);
|
RemoteCacheContext context = remoteCaches.get(cacheName);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -104,7 +105,7 @@ public class RemoteCacheInvoker {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private <K, V extends SessionEntity> void runOnRemoteCache(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long maxIdleMs, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
|
private <K, V extends SessionEntity> void runOnRemoteCache(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long maxIdleMs, K key, MergedUpdate<V> task, SessionEntityWrapper<V> sessionWrapper) {
|
||||||
final V session = sessionWrapper.getEntity();
|
final V session = sessionWrapper.getEntity();
|
||||||
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
|
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
|
||||||
|
|
||||||
|
@ -113,12 +114,14 @@ public class RemoteCacheInvoker {
|
||||||
remoteCache.remove(key);
|
remoteCache.remove(key);
|
||||||
break;
|
break;
|
||||||
case ADD:
|
case ADD:
|
||||||
remoteCache.put(key, sessionWrapper.forTransport(), task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
|
remoteCache.put(key, sessionWrapper.forTransport(),
|
||||||
|
InfinispanUtil.toHotrodTimeMs(remoteCache, task.getLifespanMs()), TimeUnit.MILLISECONDS,
|
||||||
|
InfinispanUtil.toHotrodTimeMs(remoteCache, maxIdleMs), TimeUnit.MILLISECONDS);
|
||||||
break;
|
break;
|
||||||
case ADD_IF_ABSENT:
|
case ADD_IF_ABSENT:
|
||||||
SessionEntityWrapper<V> existing = remoteCache
|
SessionEntityWrapper<V> existing = remoteCache
|
||||||
.withFlags(Flag.FORCE_RETURN_VALUE)
|
.withFlags(Flag.FORCE_RETURN_VALUE)
|
||||||
.putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
|
.putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, InfinispanUtil.toHotrodTimeMs(remoteCache, maxIdleMs), TimeUnit.MILLISECONDS);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
|
logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
|
||||||
|
|
||||||
|
@ -135,6 +138,10 @@ public class RemoteCacheInvoker {
|
||||||
|
|
||||||
|
|
||||||
private <K, V extends SessionEntity> void replace(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) {
|
private <K, V extends SessionEntity> void replace(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) {
|
||||||
|
// Adjust based on the hotrod protocol
|
||||||
|
lifespanMs = InfinispanUtil.toHotrodTimeMs(remoteCache, lifespanMs);
|
||||||
|
maxIdleMs = InfinispanUtil.toHotrodTimeMs(remoteCache, maxIdleMs);
|
||||||
|
|
||||||
boolean replaced = false;
|
boolean replaced = false;
|
||||||
int replaceIteration = 0;
|
int replaceIteration = 0;
|
||||||
while (!replaced && replaceIteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) {
|
while (!replaced && replaceIteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) {
|
||||||
|
|
|
@ -32,12 +32,20 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.connections.infinispan.TopologyInfo;
|
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||||
import org.keycloak.executors.ExecutorsProvider;
|
import org.keycloak.executors.ExecutorsProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import org.infinispan.client.hotrod.VersionedValue;
|
import org.infinispan.client.hotrod.VersionedValue;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -53,18 +61,26 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
private RemoteCache<K, SessionEntityWrapper<V>> remoteCache;
|
private RemoteCache<K, SessionEntityWrapper<V>> remoteCache;
|
||||||
private TopologyInfo topologyInfo;
|
private TopologyInfo topologyInfo;
|
||||||
private ClientListenerExecutorDecorator<K> executor;
|
private ClientListenerExecutorDecorator<K> executor;
|
||||||
|
private BiFunction<RealmModel, V, Long> lifespanMsLoader;
|
||||||
|
private BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
|
||||||
|
private KeycloakSessionFactory sessionFactory;
|
||||||
|
|
||||||
|
|
||||||
protected RemoteCacheSessionListener() {
|
protected RemoteCacheSessionListener() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache) {
|
protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache,
|
||||||
|
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.remoteCache = remoteCache;
|
this.remoteCache = remoteCache;
|
||||||
|
|
||||||
this.topologyInfo = InfinispanUtil.getTopologyInfo(session);
|
this.topologyInfo = InfinispanUtil.getTopologyInfo(session);
|
||||||
|
|
||||||
|
this.lifespanMsLoader = lifespanMsLoader;
|
||||||
|
this.maxIdleTimeMsLoader = maxIdleTimeMsLoader;
|
||||||
|
this.sessionFactory = session.getKeycloakSessionFactory();
|
||||||
|
|
||||||
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("client-listener-" + cache.getName());
|
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("client-listener-" + cache.getName());
|
||||||
this.executor = new ClientListenerExecutorDecorator<>(executor);
|
this.executor = new ClientListenerExecutorDecorator<>(executor);
|
||||||
}
|
}
|
||||||
|
@ -107,8 +123,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
|
|
||||||
// Maybe can happen under some circumstances that remoteCache doesn't yet contain the value sent in the event (maybe just theoretically...)
|
// Maybe can happen under some circumstances that remoteCache doesn't yet contain the value sent in the event (maybe just theoretically...)
|
||||||
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
|
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
|
||||||
logger.debugf("Entity '%s' not present in remoteCache. Ignoring create",
|
logger.debugf("Entity '%s' not present in remoteCache. Ignoring create", key);
|
||||||
key.toString());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,36 +131,46 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
V remoteSession = remoteSessionVersioned.getValue().getEntity();
|
V remoteSession = remoteSessionVersioned.getValue().getEntity();
|
||||||
SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(remoteSession);
|
SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(remoteSession);
|
||||||
|
|
||||||
logger.debugf("Read session entity wrapper from the remote cache: %s", remoteSession.toString());
|
logger.debugf("Read session entity wrapper from the remote cache: %s", remoteSession);
|
||||||
|
|
||||||
// Using putIfAbsent. Theoretic possibility that entity was already put to cache by someone else
|
KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
|
||||||
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
|
||||||
.putIfAbsent(key, newWrapper);
|
RealmModel realm = session.realms().getRealm(newWrapper.getEntity().getRealmId());
|
||||||
|
long lifespanMs = lifespanMsLoader.apply(realm, newWrapper.getEntity());
|
||||||
|
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, newWrapper.getEntity());
|
||||||
|
|
||||||
|
logger.tracef("Calling putIfAbsent for entity '%s' in the cache '%s' . lifespan: %d ms, maxIdleTime: %d ms", key, remoteCache.getName(), lifespanMs, maxIdleTimeMs);
|
||||||
|
|
||||||
|
// Using putIfAbsent. Theoretic possibility that entity was already put to cache by someone else
|
||||||
|
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
||||||
|
.putIfAbsent(key, newWrapper, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void replaceRemoteEntityInCache(K key, long eventVersion) {
|
protected void replaceRemoteEntityInCache(K key, long eventVersion) {
|
||||||
// TODO can be optimized and remoteSession sent in the event itself?
|
// TODO can be optimized and remoteSession sent in the event itself?
|
||||||
boolean replaced = false;
|
AtomicBoolean replaced = new AtomicBoolean(false);
|
||||||
int replaceRetries = 0;
|
int replaceRetries = 0;
|
||||||
int sleepInterval = 25;
|
int sleepInterval = 25;
|
||||||
do {
|
do {
|
||||||
replaceRetries++;
|
replaceRetries++;
|
||||||
|
|
||||||
SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
|
SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
|
||||||
VersionedValue<SessionEntityWrapper<V>> remoteSessionVersioned = remoteCache.getWithMetadata(key);
|
VersionedValue<SessionEntityWrapper<V>> remoteSessionVersioned = remoteCache.getWithMetadata(key);
|
||||||
|
|
||||||
// Probably already removed
|
// Probably already removed
|
||||||
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
|
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
|
||||||
logger.debugf("Entity '%s' not present in remoteCache. Ignoring replace",
|
logger.debugf("Entity '%s' not present in remoteCache. Ignoring replace",
|
||||||
key.toString());
|
key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteSessionVersioned.getVersion() < eventVersion) {
|
if (remoteSessionVersioned.getVersion() < eventVersion) {
|
||||||
try {
|
try {
|
||||||
logger.debugf("Got replace remote entity event prematurely for entity '%s', will try again. Event version: %d, got: %d",
|
logger.debugf("Got replace remote entity event prematurely for entity '%s', will try again. Event version: %d, got: %d",
|
||||||
key.toString(), eventVersion, remoteSessionVersioned == null ? -1 : remoteSessionVersioned.getVersion());
|
key, eventVersion, remoteSessionVersioned == null ? -1 : remoteSessionVersioned.getVersion());
|
||||||
Thread.sleep(new Random().nextInt(sleepInterval)); // using exponential backoff
|
Thread.sleep(new Random().nextInt(sleepInterval)); // using exponential backoff
|
||||||
continue;
|
continue;
|
||||||
} catch (InterruptedException ex) {
|
} catch (InterruptedException ex) {
|
||||||
|
@ -156,18 +181,26 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
}
|
}
|
||||||
SessionEntity remoteSession = remoteSessionVersioned.getValue().getEntity();
|
SessionEntity remoteSession = remoteSessionVersioned.getValue().getEntity();
|
||||||
|
|
||||||
logger.debugf("Read session entity from the remote cache: %s . replaceRetries=%d", remoteSession.toString(), replaceRetries);
|
logger.debugf("Read session entity from the remote cache: %s . replaceRetries=%d", remoteSession, replaceRetries);
|
||||||
|
|
||||||
SessionEntityWrapper<V> sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
|
SessionEntityWrapper<V> sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
|
||||||
|
|
||||||
// We received event from remoteCache, so we won't update it back
|
KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
|
||||||
replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
|
||||||
.replace(key, localEntityWrapper, sessionWrapper);
|
|
||||||
|
|
||||||
if (! replaced) {
|
RealmModel realm = session.realms().getRealm(sessionWrapper.getEntity().getRealmId());
|
||||||
logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession.toString());
|
long lifespanMs = lifespanMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||||
|
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||||
|
|
||||||
|
// We received event from remoteCache, so we won't update it back
|
||||||
|
replaced.set(cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
||||||
|
.replace(key, localEntityWrapper, sessionWrapper, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS));
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (! replaced.get()) {
|
||||||
|
logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession);
|
||||||
}
|
}
|
||||||
} while (replaceRetries < MAXIMUM_REPLACE_RETRIES && ! replaced);
|
} while (replaceRetries < MAXIMUM_REPLACE_RETRIES && ! replaced.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -203,7 +236,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
result = topologyInfo.amIOwner(cache, key);
|
result = topologyInfo.amIOwner(cache, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type.toString(), key, !result);
|
logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type, key, !result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -220,7 +253,8 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache) {
|
public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache,
|
||||||
|
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||||
/*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
|
/*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
|
||||||
|
|
||||||
// Just cluster coordinator will fetch userSessions from remote cache.
|
// Just cluster coordinator will fetch userSessions from remote cache.
|
||||||
|
@ -235,7 +269,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
RemoteCacheSessionListener<K, V> listener = new RemoteCacheSessionListener<>();
|
RemoteCacheSessionListener<K, V> listener = new RemoteCacheSessionListener<>();
|
||||||
listener.init(session, cache, remoteCache);
|
listener.init(session, cache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
|
||||||
|
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,16 @@
|
||||||
package org.keycloak.models.sessions.infinispan.util;
|
package org.keycloak.models.sessions.infinispan.util;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
|
import org.infinispan.client.hotrod.ProtocolVersion;
|
||||||
import org.infinispan.client.hotrod.RemoteCache;
|
import org.infinispan.client.hotrod.RemoteCache;
|
||||||
|
import org.infinispan.commons.api.BasicCache;
|
||||||
import org.infinispan.persistence.manager.PersistenceManager;
|
import org.infinispan.persistence.manager.PersistenceManager;
|
||||||
import org.infinispan.persistence.remote.RemoteStore;
|
import org.infinispan.persistence.remote.RemoteStore;
|
||||||
import org.infinispan.remoting.transport.Transport;
|
import org.infinispan.remoting.transport.Transport;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.connections.infinispan.TopologyInfo;
|
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -66,4 +70,26 @@ public class InfinispanUtil {
|
||||||
return transport == null || transport.isCoordinator();
|
return transport == null || transport.isCoordinator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given value to the proper value, which can be used when calling operations for the infinispan remoteCache.
|
||||||
|
*
|
||||||
|
* Infinispan HotRod protocol of versions older than 3.0 uses the "lifespan" or "maxIdle" as the normal expiration time when the value is 30 days or less.
|
||||||
|
* However for the bigger values, it assumes that the value is unix timestamp.
|
||||||
|
*
|
||||||
|
* @param ispnCache
|
||||||
|
* @param lifespanOrigMs
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long toHotrodTimeMs(BasicCache ispnCache, long lifespanOrigMs) {
|
||||||
|
if (ispnCache instanceof RemoteCache && lifespanOrigMs > 2592000000L) {
|
||||||
|
RemoteCache remoteCache = (RemoteCache) ispnCache;
|
||||||
|
ProtocolVersion protocolVersion = remoteCache.getRemoteCacheManager().getConfiguration().version();
|
||||||
|
if (ProtocolVersion.PROTOCOL_VERSION_30.compareTo(protocolVersion) > 0) {
|
||||||
|
return Time.currentTimeMillis() + lifespanOrigMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lifespanOrigMs;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.sessions.infinispan.util;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||||
|
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class SessionTimeouts {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This indicates that entry is already expired and should be removed from the cache
|
||||||
|
*/
|
||||||
|
public static final long ENTRY_EXPIRED_FLAG = -2l;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used just if timeouts are not set on the realm (usually happens just during tests when realm is created manually with the model API)
|
||||||
|
*/
|
||||||
|
public static final int MINIMAL_EXPIRATION_SEC = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum lifespan, which this userSession can remain in the infinispan cache.
|
||||||
|
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param userSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getUserSessionLifespanMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||||
|
int timeSinceSessionStart = Time.currentTime() - userSessionEntity.getStarted();
|
||||||
|
|
||||||
|
int sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
|
||||||
|
if (userSessionEntity.isRememberMe()) {
|
||||||
|
sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespanRememberMe(), sessionMaxLifespan);
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeToExpire = sessionMaxLifespan - timeSinceSessionStart;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (timeToExpire <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(timeToExpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum idle time for this userSession.
|
||||||
|
* Returned value will be used when as "maxIdleTime" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param userSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getUserSessionMaxIdleMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||||
|
int timeSinceLastRefresh = Time.currentTime() - userSessionEntity.getLastSessionRefresh();
|
||||||
|
|
||||||
|
int sessionIdleMs = Math.max(realm.getSsoSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
|
||||||
|
if (userSessionEntity.isRememberMe()) {
|
||||||
|
sessionIdleMs = Math.max(realm.getSsoSessionIdleTimeoutRememberMe(), sessionIdleMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
long maxIdleTime = sessionIdleMs - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (maxIdleTime <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(maxIdleTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum lifespan, which this clientSession can remain in the infinispan cache.
|
||||||
|
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param clientSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getClientSessionLifespanMs(RealmModel realm, AuthenticatedClientSessionEntity clientSessionEntity) {
|
||||||
|
int timeSinceTimestampUpdate = Time.currentTime() - clientSessionEntity.getTimestamp();
|
||||||
|
|
||||||
|
int sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespan(), realm.getSsoSessionMaxLifespanRememberMe());
|
||||||
|
|
||||||
|
// clientSession max lifespan has preference if set
|
||||||
|
if (realm.getClientSessionMaxLifespan() > 0) {
|
||||||
|
sessionMaxLifespan = realm.getClientSessionMaxLifespan();
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionMaxLifespan = Math.max(sessionMaxLifespan, MINIMAL_EXPIRATION_SEC);
|
||||||
|
|
||||||
|
long timeToExpire = sessionMaxLifespan - timeSinceTimestampUpdate;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (timeToExpire <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(timeToExpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maxIdle, which this clientSession will use.
|
||||||
|
* Returned value will be used as "maxIdle" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param clientSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getClientSessionMaxIdleMs(RealmModel realm, AuthenticatedClientSessionEntity clientSessionEntity) {
|
||||||
|
int timeSinceTimestampUpdate = Time.currentTime() - clientSessionEntity.getTimestamp();
|
||||||
|
|
||||||
|
int sessionIdleTimeout = Math.max(realm.getSsoSessionIdleTimeout(), realm.getSsoSessionIdleTimeoutRememberMe());
|
||||||
|
|
||||||
|
// clientSession idle timeout has preference if set
|
||||||
|
if (realm.getClientSessionIdleTimeout() > 0) {
|
||||||
|
sessionIdleTimeout = realm.getClientSessionIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionIdleTimeout = Math.max(sessionIdleTimeout, MINIMAL_EXPIRATION_SEC);
|
||||||
|
|
||||||
|
long timeToExpire = sessionIdleTimeout - timeSinceTimestampUpdate + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (timeToExpire <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(timeToExpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum lifespan, which this offline userSession can remain in the infinispan cache.
|
||||||
|
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param userSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getOfflineSessionLifespanMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||||
|
// By default, this is disabled, so offlineSessions have just "maxIdle"
|
||||||
|
if (!realm.isOfflineSessionMaxLifespanEnabled()) return -1l;
|
||||||
|
|
||||||
|
int timeSinceSessionStart = Time.currentTime() - userSessionEntity.getStarted();
|
||||||
|
|
||||||
|
int sessionMaxLifespan = Math.max(realm.getOfflineSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
|
||||||
|
|
||||||
|
long timeToExpire = sessionMaxLifespan - timeSinceSessionStart;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (timeToExpire <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(timeToExpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum idle time for this offline userSession.
|
||||||
|
* Returned value will be used when as "maxIdleTime" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param userSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getOfflineSessionMaxIdleMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||||
|
int timeSinceLastRefresh = Time.currentTime() - userSessionEntity.getLastSessionRefresh();
|
||||||
|
|
||||||
|
int sessionIdle = Math.max(realm.getOfflineSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
|
||||||
|
|
||||||
|
long maxIdleTime = sessionIdle - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (maxIdleTime <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(maxIdleTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum lifespan, which this offline clientSession can remain in the infinispan cache.
|
||||||
|
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param authenticatedClientSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getOfflineClientSessionLifespanMs(RealmModel realm, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
|
||||||
|
// By default, this is disabled, so offlineSessions have just "maxIdle"
|
||||||
|
if (!realm.isOfflineSessionMaxLifespanEnabled() && realm.getClientOfflineSessionMaxLifespan() <= 0) return -1l;
|
||||||
|
|
||||||
|
int timeSinceTimestamp = Time.currentTime() - authenticatedClientSessionEntity.getTimestamp();
|
||||||
|
|
||||||
|
int sessionMaxLifespan = Math.max(realm.getOfflineSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
|
||||||
|
|
||||||
|
// clientSession max lifespan has preference if set
|
||||||
|
if (realm.getClientOfflineSessionMaxLifespan() > 0) {
|
||||||
|
sessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeToExpire = sessionMaxLifespan - timeSinceTimestamp;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (timeToExpire <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(timeToExpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maxIdle, which this offline clientSession will use.
|
||||||
|
* Returned value will be used as "maxIdle" when calling put/replace operation in the infinispan cache for this entity
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param authenticatedClientSessionEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getOfflineClientSessionMaxIdleMs(RealmModel realm, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
|
||||||
|
int timeSinceLastRefresh = Time.currentTime() - authenticatedClientSessionEntity.getTimestamp();
|
||||||
|
|
||||||
|
int sessionIdle = Math.max(realm.getOfflineSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
|
||||||
|
|
||||||
|
// clientSession idle timeout has preference if set
|
||||||
|
if (realm.getClientOfflineSessionIdleTimeout() > 0) {
|
||||||
|
sessionIdle = realm.getClientOfflineSessionIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
long maxIdleTime = sessionIdle - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||||
|
|
||||||
|
// Indication that entry should be expired
|
||||||
|
if (maxIdleTime <=0) {
|
||||||
|
return ENTRY_EXPIRED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time.toMillis(maxIdleTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not using lifespan for detached login failure (backwards compatibility with the background cleaner threads, which were used for cleanup of detached login failures)
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param loginFailureEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getLoginFailuresLifespanMs(RealmModel realm, LoginFailureEntity loginFailureEntity) {
|
||||||
|
return -1l;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not using maxIdle for detached login failure (backwards compatibility with the background cleaner threads, which were used for cleanup of detached login failures)
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param loginFailureEntity
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getLoginFailuresMaxIdleMs(RealmModel realm, LoginFailureEntity loginFailureEntity) {
|
||||||
|
return -1l;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
|
@ -154,6 +155,10 @@ public class ConcurrencyJDGCacheReplaceTest {
|
||||||
// Create caches, listeners and finally worker threads
|
// Create caches, listeners and finally worker threads
|
||||||
remoteCache1 = InfinispanUtil.getRemoteCache(cache1);
|
remoteCache1 = InfinispanUtil.getRemoteCache(cache1);
|
||||||
remoteCache2 = InfinispanUtil.getRemoteCache(cache2);
|
remoteCache2 = InfinispanUtil.getRemoteCache(cache2);
|
||||||
|
|
||||||
|
// Manual test of lifespans
|
||||||
|
testLifespans();
|
||||||
|
|
||||||
Thread worker1 = createWorker(cache1, 1);
|
Thread worker1 = createWorker(cache1, 1);
|
||||||
Thread worker2 = createWorker(cache2, 2);
|
Thread worker2 = createWorker(cache2, 2);
|
||||||
|
|
||||||
|
@ -375,6 +380,30 @@ public class ConcurrencyJDGCacheReplaceTest {
|
||||||
private enum ReplaceStatus {
|
private enum ReplaceStatus {
|
||||||
REPLACED, NOT_REPLACED, ERROR
|
REPLACED, NOT_REPLACED, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void testLifespans() throws Exception {
|
||||||
|
long l1 = InfinispanUtil.toHotrodTimeMs(remoteCache1, 5000);
|
||||||
|
long l2 = InfinispanUtil.toHotrodTimeMs(remoteCache2, 2592000000L);
|
||||||
|
long l3 = InfinispanUtil.toHotrodTimeMs(remoteCache2, 2592000001L);
|
||||||
|
//long l4 = InfinispanUtil.getLifespanMs(remoteCache1, Time.currentTimeMillis() + 5000);
|
||||||
|
|
||||||
|
remoteCache1.put("k1", "v1", l1, TimeUnit.MILLISECONDS);
|
||||||
|
remoteCache1.put("k2", "v2", l2, TimeUnit.MILLISECONDS);
|
||||||
|
remoteCache1.put("k3", "v3", l3, TimeUnit.MILLISECONDS);
|
||||||
|
remoteCache1.put("k4", "v4", Time.currentTimeMillis() + 5000, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
System.out.println("l1=" + l1 + ", l2=" + l2 + ", l3=" + l3);
|
||||||
|
System.out.println("k1=" + remoteCache1.get("k1") + ", k2=" + remoteCache1.get("k2") + ", k3=" + remoteCache1.get("k3") + ", k4=" + remoteCache1.get("k4"));
|
||||||
|
|
||||||
|
Thread.sleep(4000);
|
||||||
|
|
||||||
|
System.out.println("k1=" + remoteCache1.get("k1") + ", k2=" + remoteCache1.get("k2") + ", k3=" + remoteCache1.get("k3") + ", k4=" + remoteCache1.get("k4"));
|
||||||
|
|
||||||
|
Thread.sleep(2000);
|
||||||
|
|
||||||
|
System.out.println("k1=" + remoteCache1.get("k1") + ", k2=" + remoteCache1.get("k2") + ", k3=" + remoteCache1.get("k3") + ", k4=" + remoteCache1.get("k4"));
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
// Worker, which operates on "classic" cache and rely on operations delegated to the second cache
|
// Worker, which operates on "classic" cache and rely on operations delegated to the second cache
|
||||||
private static class CacheWorker extends Thread {
|
private static class CacheWorker extends Thread {
|
||||||
|
|
|
@ -131,6 +131,11 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi
|
||||||
tx.delete(UUID.fromString(authenticationSession.getId()));
|
tx.delete(UUID.fromString(authenticationSession.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllExpired() {
|
||||||
|
session.realms().getRealmsStream().forEach(this::removeExpired);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpired(RealmModel realm) {
|
public void removeExpired(RealmModel realm) {
|
||||||
Objects.requireNonNull(realm, "The provided realm can't be null!");
|
Objects.requireNonNull(realm, "The provided realm can't be null!");
|
||||||
|
|
|
@ -138,6 +138,11 @@ public interface UserSessionProvider extends Provider {
|
||||||
void removeUserSession(RealmModel realm, UserSessionModel session);
|
void removeUserSession(RealmModel realm, UserSessionModel session);
|
||||||
void removeUserSessions(RealmModel realm, UserModel user);
|
void removeUserSessions(RealmModel realm, UserModel user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove expired user sessions and client sessions in all the realms
|
||||||
|
*/
|
||||||
|
void removeAllExpired();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes expired user sessions owned by this realm from this provider.
|
* Removes expired user sessions owned by this realm from this provider.
|
||||||
* If this `UserSessionProvider` uses `UserSessionPersister`, the removal of the expired
|
* If this `UserSessionProvider` uses `UserSessionPersister`, the removal of the expired
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.sessions;
|
package org.keycloak.sessions;
|
||||||
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
|
@ -71,6 +72,11 @@ public interface AuthenticationSessionProvider extends Provider {
|
||||||
*/
|
*/
|
||||||
void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession);
|
void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove expired authentication sessions in all the realms
|
||||||
|
*/
|
||||||
|
void removeAllExpired();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all expired root authentication sessions for the given realm.
|
* Removes all expired root authentication sessions for the given realm.
|
||||||
* @param realm {@code RealmModel} Can't be {@code null}.
|
* @param realm {@code RealmModel} Can't be {@code null}.
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.keycloak.services.scheduled;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.UserSessionProvider;
|
|
||||||
import org.keycloak.timer.ScheduledTask;
|
import org.keycloak.timer.ScheduledTask;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,11 +35,8 @@ public class ClearExpiredUserSessions implements ScheduledTask {
|
||||||
public void run(KeycloakSession session) {
|
public void run(KeycloakSession session) {
|
||||||
long currentTimeMillis = Time.currentTimeMillis();
|
long currentTimeMillis = Time.currentTimeMillis();
|
||||||
|
|
||||||
UserSessionProvider sessions = session.sessions();
|
session.authenticationSessions().removeAllExpired();
|
||||||
session.realms().getRealmsStream().forEach(realm -> {
|
session.sessions().removeAllExpired();
|
||||||
sessions.removeExpired(realm);
|
|
||||||
session.authenticationSessions().removeExpired(realm);
|
|
||||||
});
|
|
||||||
|
|
||||||
long took = Time.currentTimeMillis() - currentTimeMillis;
|
long took = Time.currentTimeMillis() - currentTimeMillis;
|
||||||
logger.debugf("ClearExpiredUserSessions finished in %d ms", took);
|
logger.debugf("ClearExpiredUserSessions finished in %d ms", took);
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.model.infinispan;
|
||||||
|
|
||||||
|
import org.infinispan.commons.time.TimeService;
|
||||||
|
import org.infinispan.factories.GlobalComponentRegistry;
|
||||||
|
import org.infinispan.factories.impl.BasicComponentRegistry;
|
||||||
|
import org.infinispan.factories.impl.ComponentRef;
|
||||||
|
import org.infinispan.manager.EmbeddedCacheManager;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class InfinispanTestUtil {
|
||||||
|
|
||||||
|
protected static final Logger logger = Logger.getLogger(InfinispanTestUtil.class);
|
||||||
|
|
||||||
|
private static TimeService origTimeService = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Keycloak test TimeService to infinispan cacheManager. This will cause that infinispan will be aware of Keycloak Time offset, which is useful
|
||||||
|
* for testing that infinispan entries are expired after moving Keycloak time forward with {@link org.keycloak.common.util.Time#setOffset} .
|
||||||
|
*/
|
||||||
|
public static void setTestingTimeService(KeycloakSession session) {
|
||||||
|
// Testing timeService already set. This shouldn't happen if this utility is properly used
|
||||||
|
if (origTimeService != null) {
|
||||||
|
throw new IllegalStateException("Calling setTestingTimeService when testing TimeService was already set");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Will set KeycloakIspnTimeService to the infinispan cacheManager");
|
||||||
|
|
||||||
|
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
|
EmbeddedCacheManager cacheManager = ispnProvider.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getCacheManager();
|
||||||
|
origTimeService = replaceComponent(cacheManager, TimeService.class, new KeycloakTestTimeService(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void revertTimeService(KeycloakSession session) {
|
||||||
|
// Testing timeService not set. This shouldn't happen if this utility is properly used
|
||||||
|
if (origTimeService == null) {
|
||||||
|
throw new IllegalStateException("Calling revertTimeService when testing TimeService was not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Revert set KeycloakIspnTimeService to the infinispan cacheManager");
|
||||||
|
|
||||||
|
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
|
EmbeddedCacheManager cacheManager = ispnProvider.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getCacheManager();
|
||||||
|
replaceComponent(cacheManager, TimeService.class, origTimeService, true);
|
||||||
|
origTimeService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forked from org.infinispan.test.TestingUtil class
|
||||||
|
*
|
||||||
|
* Replaces a component in a running cache manager (global component registry).
|
||||||
|
*
|
||||||
|
* @param cacheMgr cache in which to replace component
|
||||||
|
* @param componentType component type of which to replace
|
||||||
|
* @param replacementComponent new instance
|
||||||
|
* @param rewire if true, ComponentRegistry.rewire() is called after replacing.
|
||||||
|
*
|
||||||
|
* @return the original component that was replaced
|
||||||
|
*/
|
||||||
|
private static <T> T replaceComponent(EmbeddedCacheManager cacheMgr, Class<T> componentType, T replacementComponent, boolean rewire) {
|
||||||
|
GlobalComponentRegistry cr = cacheMgr.getGlobalComponentRegistry();
|
||||||
|
BasicComponentRegistry bcr = cr.getComponent(BasicComponentRegistry.class);
|
||||||
|
ComponentRef<T> old = bcr.getComponent(componentType);
|
||||||
|
bcr.replaceComponent(componentType.getName(), replacementComponent, true);
|
||||||
|
if (rewire) {
|
||||||
|
cr.rewire();
|
||||||
|
cr.rewireNamedRegistries();
|
||||||
|
}
|
||||||
|
return old != null ? old.wired() : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.model.infinispan;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.infinispan.util.EmbeddedTimeService;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infinispan TimeService, which delegates to Keycloak Time.currentTime to figure current time. Useful for testing purposes.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class KeycloakTestTimeService extends EmbeddedTimeService {
|
||||||
|
|
||||||
|
private long getCurrentTimeMillis() {
|
||||||
|
return Time.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long wallClockTime() {
|
||||||
|
return getCurrentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long time() {
|
||||||
|
return TimeUnit.MILLISECONDS.toNanos(getCurrentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant instant() {
|
||||||
|
return Instant.ofEpochMilli(getCurrentTimeMillis());
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,6 +66,7 @@ import org.keycloak.testsuite.events.TestEventsListenerProvider;
|
||||||
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
|
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
|
||||||
import org.keycloak.testsuite.forms.PassThroughAuthenticator;
|
import org.keycloak.testsuite.forms.PassThroughAuthenticator;
|
||||||
import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
|
import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
|
||||||
|
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
|
||||||
import org.keycloak.testsuite.rest.representation.AuthenticatorState;
|
import org.keycloak.testsuite.rest.representation.AuthenticatorState;
|
||||||
import org.keycloak.testsuite.rest.resource.TestCacheResource;
|
import org.keycloak.testsuite.rest.resource.TestCacheResource;
|
||||||
import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
|
import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
|
||||||
|
@ -181,6 +182,22 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/set-testing-infinispan-time-service")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response setTestingInfinispanTimeService() {
|
||||||
|
InfinispanTestUtil.setTestingTimeService(session);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/revert-testing-infinispan-time-service")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response revertTestingInfinispanTimeService() {
|
||||||
|
InfinispanTestUtil.revertTimeService(session);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/get-client-sessions-count")
|
@Path("/get-client-sessions-count")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -107,6 +107,13 @@ public class TestCacheResource {
|
||||||
cache.remove(id);
|
cache.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/process-expiration")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public void processExpiration() {
|
||||||
|
cache.getAdvancedCache().getExpirationManager().processExpiration();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/jgroups-stats")
|
@Path("/jgroups-stats")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
<module name="org.keycloak.keycloak-model-jpa"/>
|
<module name="org.keycloak.keycloak-model-jpa"/>
|
||||||
<module name="org.keycloak.keycloak-ldap-federation"/>
|
<module name="org.keycloak.keycloak-ldap-federation"/>
|
||||||
<module name="org.infinispan"/>
|
<module name="org.infinispan"/>
|
||||||
|
<module name="org.infinispan.commons"/>
|
||||||
<module name="org.infinispan.client.hotrod"/>
|
<module name="org.infinispan.client.hotrod"/>
|
||||||
<module name="org.jgroups"/>
|
<module name="org.jgroups"/>
|
||||||
<module name="org.jboss.logging"/>
|
<module name="org.jboss.logging"/>
|
||||||
|
|
|
@ -66,6 +66,15 @@ public interface TestingCacheResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
void removeKey(@PathParam("id") String id);
|
void removeKey(@PathParam("id") String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce calling of the expiration on the particular infinispan cache. This will immediately expire the expired cache entries, so that they won't be available in the cache.
|
||||||
|
* Without calling this, expired entries would be removed by the infinispan expiration (probably by infinispan periodic background cleaner task)
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/process-expiration")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
void processExpiration();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/jgroups-stats")
|
@Path("/jgroups-stats")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -196,6 +196,20 @@ public interface TestingResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
void removeExpired(@QueryParam("realm") final String realm);
|
void removeExpired(@QueryParam("realm") final String realm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will set {@link org.keycloak.testsuite.model.infinispan.KeycloakTestTimeService} to the infinispan CacheManager before the test.
|
||||||
|
* This will allow infinispan expiration to be aware of Keycloak {@link org.keycloak.common.util.Time#setOffset}
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/set-testing-infinispan-time-service")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
void setTestingInfinispanTimeService();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/revert-testing-infinispan-time-service")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
void revertTestingInfinispanTimeService();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/get-client-sessions-count")
|
@Path("/get-client-sessions-count")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -37,6 +37,8 @@ import org.keycloak.testsuite.arquillian.CrossDCTestEnricher;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.InitialDcState;
|
import org.keycloak.testsuite.arquillian.annotation.InitialDcState;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.arquillian.CrossDCTestEnricher.forAllBackendNodesStream;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract cross-data-centre test that defines primitives for handling cross-DC setup.
|
* Abstract cross-data-centre test that defines primitives for handling cross-DC setup.
|
||||||
|
@ -221,4 +223,26 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
||||||
super.resetTimeOffset();
|
super.resetTimeOffset();
|
||||||
setTimeOffsetOnAllStartedContainers(0);
|
setTimeOffsetOnAllStartedContainers(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setInfinispanTestTimeServiceOnAllStartedAuthServers() {
|
||||||
|
forAllBackendNodesStream()
|
||||||
|
.filter(ContainerInfo::isStarted)
|
||||||
|
.forEach(this::setInfinispanTestTimeServiceonAuthServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInfinispanTestTimeServiceonAuthServer(ContainerInfo backendAuthServer) {
|
||||||
|
log.infof("Set Infinispan Test Time Service for backend server %s", backendAuthServer.getQualifier());
|
||||||
|
getTestingClientFor(backendAuthServer).testing().setTestingInfinispanTimeService();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void revertInfinispanTestTimeServiceOnAllStartedAuthServers() {
|
||||||
|
forAllBackendNodesStream()
|
||||||
|
.filter(ContainerInfo::isStarted)
|
||||||
|
.forEach(this::revertInfinispanTestTimeServiceonAuthServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revertInfinispanTestTimeServiceonAuthServer(ContainerInfo backendAuthServer) {
|
||||||
|
log.infof("Revert Infinispan Test Time Service for backend server %s", backendAuthServer.getQualifier());
|
||||||
|
getTestingClientFor(backendAuthServer).testing().revertTestingInfinispanTimeService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,115 +124,127 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats
|
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
// Set the infinispan testTimeService on all started auth servers
|
||||||
|
setInfinispanTestTimeServiceOnAllStartedAuthServers();
|
||||||
|
|
||||||
// Ensure to remove all current sessions and offline sessions
|
|
||||||
setTimeOffset(10000000);
|
|
||||||
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
|
||||||
setTimeOffset(0);
|
|
||||||
|
|
||||||
sessionCacheDc1Stats.reset();
|
|
||||||
sessionCacheDc2Stats.reset();
|
|
||||||
clientSessionCacheDc1Stats.reset();
|
|
||||||
clientSessionCacheDc2Stats.reset();
|
|
||||||
|
|
||||||
// Disable DC2 on loadbalancer
|
|
||||||
disableDcOnLoadBalancer(DC.SECOND);
|
|
||||||
|
|
||||||
// Get statistics
|
|
||||||
AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
|
|
||||||
AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
|
|
||||||
AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
|
|
||||||
AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
|
|
||||||
AtomicInteger lsrDc1 = new AtomicInteger(-1);
|
|
||||||
AtomicInteger lsrDc2 = new AtomicInteger(-1);
|
|
||||||
|
|
||||||
// Login
|
|
||||||
OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
|
|
||||||
String code = response1.getCode();
|
|
||||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
|
||||||
Assert.assertNotNull(tokenResponse.getAccessToken());
|
|
||||||
String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
|
|
||||||
String refreshToken1 = tokenResponse.getRefreshToken();
|
|
||||||
|
|
||||||
// Assert statistics - sessions created on both DCs and created on remoteCaches too
|
|
||||||
assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
|
||||||
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
|
||||||
lsrDc1, lsrDc2, true, true, true, false);
|
|
||||||
|
|
||||||
|
|
||||||
// Set time offset
|
|
||||||
setTimeOffset(100);
|
|
||||||
|
|
||||||
// refresh token on DC1
|
|
||||||
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
|
|
||||||
String refreshToken3 = tokenResponse.getRefreshToken();
|
|
||||||
Assert.assertNotNull(refreshToken3);
|
|
||||||
|
|
||||||
// Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches not updated
|
|
||||||
assertStatistics("After refresh at time 100", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
|
||||||
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
|
||||||
lsrDc1, lsrDc2, true, true, false, false);
|
|
||||||
|
|
||||||
|
|
||||||
// Set time offset
|
|
||||||
setTimeOffset(110);
|
|
||||||
|
|
||||||
// refresh token on DC1
|
|
||||||
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
|
|
||||||
String refreshToken2 = tokenResponse.getRefreshToken();
|
|
||||||
Assert.assertNotNull(refreshToken2);
|
|
||||||
|
|
||||||
// Assert statistics - sessions updated just on DC1.
|
|
||||||
// Update of DC2 is postponed (It's just 10 seconds since last message). RemoteCaches not updated
|
|
||||||
assertStatistics("After refresh at time 110", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
|
||||||
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
|
||||||
lsrDc1, lsrDc2, true, false, false, false);
|
|
||||||
|
|
||||||
|
|
||||||
// 31 minutes after "100". Session should be still valid and not yet expired (RefreshToken will be invalid due the expiration on the JWT itself. Hence not testing refresh here)
|
|
||||||
setTimeOffset(1960);
|
|
||||||
|
|
||||||
boolean sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
|
|
||||||
RealmModel realm = session.realms().getRealmByName("test");
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
|
||||||
return AuthenticationManager.isSessionValid(realm, userSession);
|
|
||||||
}, Boolean.class);
|
|
||||||
|
|
||||||
Assert.assertTrue(sessionValid);
|
|
||||||
|
|
||||||
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
|
||||||
|
|
||||||
// Assert statistics - nothing was updated. No refresh happened and nothing was cleared during "removeExpired"
|
|
||||||
assertStatistics("After checking valid at time 1960", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
|
||||||
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
|
||||||
lsrDc1, lsrDc2, false, false, false, false);
|
|
||||||
|
|
||||||
|
|
||||||
// 35 minutes after "100". Session not valid and will be expired by the cleaner
|
|
||||||
setTimeOffset(2200);
|
|
||||||
|
|
||||||
sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
|
|
||||||
RealmModel realm = session.realms().getRealmByName("test");
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
|
||||||
return AuthenticationManager.isSessionValid(realm, userSession);
|
|
||||||
}, Boolean.class);
|
|
||||||
|
|
||||||
Assert.assertFalse(sessionValid);
|
|
||||||
|
|
||||||
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
|
||||||
|
|
||||||
// Session should be removed on both DCs
|
|
||||||
try {
|
try {
|
||||||
getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId, false);
|
// Ensure to remove all current sessions and offline sessions
|
||||||
Assert.fail("It wasn't expected to find the session " + sessionId);
|
setTimeOffset(10000000);
|
||||||
} catch (NotFoundException nfe) {
|
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
||||||
// Expected
|
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
||||||
}
|
setTimeOffset(0);
|
||||||
try {
|
|
||||||
getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId, false);
|
sessionCacheDc1Stats.reset();
|
||||||
Assert.fail("It wasn't expected to find the session " + sessionId);
|
sessionCacheDc2Stats.reset();
|
||||||
} catch (NotFoundException nfe) {
|
clientSessionCacheDc1Stats.reset();
|
||||||
// Expected
|
clientSessionCacheDc2Stats.reset();
|
||||||
|
|
||||||
|
// Disable DC2 on loadbalancer
|
||||||
|
disableDcOnLoadBalancer(DC.SECOND);
|
||||||
|
|
||||||
|
// Get statistics
|
||||||
|
AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
|
||||||
|
AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
|
||||||
|
AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
|
||||||
|
AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
|
||||||
|
AtomicInteger lsrDc1 = new AtomicInteger(-1);
|
||||||
|
AtomicInteger lsrDc2 = new AtomicInteger(-1);
|
||||||
|
|
||||||
|
// Login
|
||||||
|
OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
|
||||||
|
String code = response1.getCode();
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
Assert.assertNotNull(tokenResponse.getAccessToken());
|
||||||
|
String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
|
||||||
|
String refreshToken1 = tokenResponse.getRefreshToken();
|
||||||
|
|
||||||
|
// Assert statistics - sessions created on both DCs and created on remoteCaches too
|
||||||
|
assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
||||||
|
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
||||||
|
lsrDc1, lsrDc2, true, true, true, false);
|
||||||
|
|
||||||
|
|
||||||
|
// Set time offset
|
||||||
|
setTimeOffset(100);
|
||||||
|
|
||||||
|
// refresh token on DC1
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
|
||||||
|
String refreshToken2 = tokenResponse.getRefreshToken();
|
||||||
|
Assert.assertNotNull(refreshToken2);
|
||||||
|
|
||||||
|
// Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches not updated
|
||||||
|
assertStatistics("After refresh at time 100", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
||||||
|
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
||||||
|
lsrDc1, lsrDc2, true, true, false, false);
|
||||||
|
|
||||||
|
|
||||||
|
// Set time offset
|
||||||
|
setTimeOffset(110);
|
||||||
|
|
||||||
|
// refresh token on DC1
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
|
||||||
|
String refreshToken3 = tokenResponse.getRefreshToken();
|
||||||
|
Assert.assertNotNull(refreshToken3);
|
||||||
|
|
||||||
|
// Assert statistics - sessions updated just on DC1.
|
||||||
|
// Update of DC2 is postponed (It's just 10 seconds since last message). RemoteCaches not updated
|
||||||
|
assertStatistics("After refresh at time 110", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
||||||
|
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
||||||
|
lsrDc1, lsrDc2, true, false, false, false);
|
||||||
|
|
||||||
|
|
||||||
|
// 31 minutes after "100". Session should be still valid and not yet expired (RefreshToken will be invalid due the expiration on the JWT itself. Hence not testing refresh here)
|
||||||
|
setTimeOffset(1960);
|
||||||
|
|
||||||
|
boolean sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
||||||
|
return AuthenticationManager.isSessionValid(realm, userSession);
|
||||||
|
}, Boolean.class);
|
||||||
|
|
||||||
|
Assert.assertTrue(sessionValid);
|
||||||
|
|
||||||
|
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
||||||
|
|
||||||
|
// Assert statistics - nothing was updated. No refresh happened and nothing was cleared during "removeExpired"
|
||||||
|
assertStatistics("After checking valid at time 1960", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
|
||||||
|
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
|
||||||
|
lsrDc1, lsrDc2, false, false, false, false);
|
||||||
|
|
||||||
|
|
||||||
|
// 35 minutes after "100". Session not valid and will be expired by the cleaner
|
||||||
|
setTimeOffset(2200);
|
||||||
|
|
||||||
|
sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
||||||
|
return AuthenticationManager.isSessionValid(realm, userSession);
|
||||||
|
}, Boolean.class);
|
||||||
|
|
||||||
|
Assert.assertFalse(sessionValid);
|
||||||
|
|
||||||
|
// 2000 seconds after the previous. This should ensure that session would be expired from the cache due the invalid maxIdle.
|
||||||
|
// Previous read at time 2200 "refreshed" the maxIdle in the infinispan cache. This shouldn't happen in reality as an attempt to call refreshToken request on invalid session does backchannelLogout
|
||||||
|
setTimeOffset(4200);
|
||||||
|
|
||||||
|
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
||||||
|
|
||||||
|
// Session should be removed on both DCs
|
||||||
|
try {
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId, false);
|
||||||
|
Assert.fail("It wasn't expected to find the session " + sessionId);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId, false);
|
||||||
|
Assert.fail("It wasn't expected to find the session " + sessionId);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Revert time service
|
||||||
|
revertInfinispanTestTimeServiceOnAllStartedAuthServers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +260,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
// Ensure to remove all current sessions and offline sessions
|
// Ensure to remove all current sessions and offline sessions
|
||||||
setTimeOffset(10000000);
|
setTimeOffset(10000000);
|
||||||
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
||||||
|
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
||||||
setTimeOffset(0);
|
setTimeOffset(0);
|
||||||
|
|
||||||
sessionCacheDc1Stats.reset();
|
sessionCacheDc1Stats.reset();
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
@ -99,12 +100,30 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
RealmRepresentation realmRep = RealmBuilder.create()
|
RealmRepresentation realmRep = RealmBuilder.create()
|
||||||
|
.ssoSessionIdleTimeout(300)
|
||||||
|
.ssoSessionMaxLifespan(600)
|
||||||
|
.offlineSessionIdleTimeout(900)
|
||||||
.name(REALM_NAME)
|
.name(REALM_NAME)
|
||||||
.user(user)
|
.user(user)
|
||||||
.client(client)
|
.client(client)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
adminClient.realms().create(realmRep);
|
adminClient.realms().create(realmRep);
|
||||||
|
|
||||||
|
setInfinispanTestTimeServiceOnAllStartedAuthServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterTest() {
|
||||||
|
// Expire all sessions
|
||||||
|
setTimeOffset(1500);
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
resetTimeOffset();
|
||||||
|
|
||||||
|
revertInfinispanTestTimeServiceOnAllStartedAuthServers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -195,7 +214,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
|
int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
|
||||||
// Needs to use "received_messages" on Infinispan 9.2.4.Final. Stats for "sent_messages" is always null
|
// Needs to use "received_messages" on Infinispan 9.2.4.Final. Stats for "sent_messages" is always null
|
||||||
long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_RECEIVED_MESSAGES);
|
long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_RECEIVED_MESSAGES);
|
||||||
log.infof(messagePrefix + ": sessions1: %d, sessions2: %d, remoteSessions1: %d, remoteSessions2: %d, sentMessages: %d", sessions1, sessions2, remoteSessions1, remoteSessions2, messagesCount);
|
log.infof(messagePrefix + ": sessions1: %d, sessions2: %d, clientSessions1: %d, clientSessions2: %d, remoteSessions1: %d, remoteSessions2: %d, sentMessages: %d", sessions1, sessions2, clientSessions1, clientSessions2, remoteSessions1, remoteSessions2, messagesCount);
|
||||||
|
|
||||||
Assert.assertEquals(sessions1, sessions1Expected);
|
Assert.assertEquals(sessions1, sessions1Expected);
|
||||||
Assert.assertEquals(sessions2, sessions2Expected);
|
Assert.assertEquals(sessions2, sessions2Expected);
|
||||||
|
@ -275,8 +294,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
|
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
|
||||||
|
|
||||||
|
|
||||||
// Set time offset
|
// Increase offset to 10 minutes (SSO Session Idle is 5 minutes for the realm). To make sure that admin session from master realm won't expire
|
||||||
setTimeOffset(10000000);
|
setTimeOffset(610);
|
||||||
|
|
||||||
// Assert I am not able to refresh anymore
|
// Assert I am not able to refresh anymore
|
||||||
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
|
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
|
||||||
|
@ -287,7 +306,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
channelStatisticsCrossDc.reset();
|
channelStatisticsCrossDc.reset();
|
||||||
|
|
||||||
// Remove expired in DC0
|
// Remove expired in DC0
|
||||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
|
||||||
// Assert sessions removed on node1 and node2 and on remote caches.
|
// Assert sessions removed on node1 and node2 and on remote caches.
|
||||||
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
|
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
|
||||||
|
@ -312,7 +332,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
channelStatisticsCrossDc.reset();
|
channelStatisticsCrossDc.reset();
|
||||||
|
|
||||||
// Remove expired in DC0
|
// Remove expired in DC0
|
||||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
|
||||||
// Nothing yet expired. It may happen that no message sent between DCs
|
// Nothing yet expired. It may happen that no message sent between DCs
|
||||||
assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
||||||
|
@ -321,8 +341,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
|
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
|
||||||
|
|
||||||
|
|
||||||
// Set time offset
|
// Increase offset to 20 minutes. Client offline sessions should be expired from infinispan by that. Master sessions will still remain there.
|
||||||
setTimeOffset(10000000);
|
setTimeOffset(1210);
|
||||||
|
|
||||||
// Assert I am not able to refresh anymore
|
// Assert I am not able to refresh anymore
|
||||||
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
|
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
|
||||||
|
@ -333,7 +353,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
channelStatisticsCrossDc.reset();
|
channelStatisticsCrossDc.reset();
|
||||||
|
|
||||||
// Remove expired in DC0
|
// Remove expired in DC0
|
||||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||||
|
|
||||||
// Assert sessions removed on node1 and node2 and on remote caches.
|
// Assert sessions removed on node1 and node2 and on remote caches.
|
||||||
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
||||||
|
@ -555,12 +576,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).removeKey(userSessionId);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).removeKey(userSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increase offset to big value like 100 hours
|
// Increase offset to 10 minutes (SSO Session Idle is 5 minutes for the realm). To make sure that admin session from master realm won't expire
|
||||||
setTimeOffset(360000);
|
setTimeOffset(610);
|
||||||
|
|
||||||
// Trigger removeExpired
|
// Trigger removeExpired
|
||||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||||
getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
|
|
||||||
|
|
||||||
// Ensure clientSessions were removed
|
// Ensure clientSessions were removed
|
||||||
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
|
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
|
||||||
|
@ -586,12 +606,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).removeKey(userSessionId);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).removeKey(userSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increase offset to big value like 10000 hours (400+ days)
|
// Increase offset to 20 minutes. Client offline sessions should be expired from infinispan by that. Master sessions will still remain there.
|
||||||
setTimeOffset(36000000);
|
setTimeOffset(1210);
|
||||||
|
|
||||||
// Trigger removeExpired
|
// Trigger removeExpired
|
||||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||||
getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
|
|
||||||
|
|
||||||
// Ensure clientSessions were removed
|
// Ensure clientSessions were removed
|
||||||
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
||||||
|
|
|
@ -770,23 +770,28 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
// KEYCLOAK-1037
|
// KEYCLOAK-1037
|
||||||
@Test
|
@Test
|
||||||
public void loginExpiredCodeWithExplicitRemoveExpired() {
|
public void loginExpiredCodeWithExplicitRemoveExpired() {
|
||||||
loginPage.open();
|
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
setTimeOffset(5000);
|
|
||||||
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
|
|
||||||
testingClient.testing().removeExpired("test");
|
|
||||||
|
|
||||||
loginPage.login("login@test.com", "password");
|
try {
|
||||||
|
loginPage.open();
|
||||||
|
setTimeOffset(5000);
|
||||||
|
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
|
||||||
|
testingClient.testing().removeExpired("test");
|
||||||
|
|
||||||
//loginPage.assertCurrent();
|
loginPage.login("login@test.com", "password");
|
||||||
loginPage.assertCurrent();
|
|
||||||
|
|
||||||
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
|
//loginPage.assertCurrent();
|
||||||
setTimeOffset(0);
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
|
||||||
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
|
||||||
.client((String) null)
|
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||||
.assertEvent();
|
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
||||||
|
.client((String) null)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.model;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -45,7 +46,8 @@ import static org.hamcrest.core.IsNull.notNullValue;
|
||||||
import static org.hamcrest.core.IsNull.nullValue;
|
import static org.hamcrest.core.IsNull.nullValue;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +56,9 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
|
||||||
@AuthServerContainerExclude(REMOTE)
|
@AuthServerContainerExclude(REMOTE)
|
||||||
public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
testingClient.server().run(session -> {
|
testingClient.server().run(session -> {
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
@ -41,6 +42,7 @@ import org.keycloak.services.managers.UserSessionManager;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
|
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
|
||||||
|
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||||
import org.keycloak.timer.TimerProvider;
|
import org.keycloak.timer.TimerProvider;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -61,6 +63,10 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
|
||||||
*/
|
*/
|
||||||
@AuthServerContainerExclude(AuthServer.REMOTE)
|
@AuthServerContainerExclude(AuthServer.REMOTE)
|
||||||
public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest {
|
public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||||
|
|
||||||
private static KeycloakSession currentSession;
|
private static KeycloakSession currentSession;
|
||||||
private static RealmModel realm;
|
private static RealmModel realm;
|
||||||
private static UserSessionManager sessionManager;
|
private static UserSessionManager sessionManager;
|
||||||
|
@ -545,7 +551,7 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
|
||||||
Assert.assertEquals(1, persister.getUserSessionsCount(true));
|
Assert.assertEquals(1, persister.getUserSessionsCount(true));
|
||||||
|
|
||||||
// Expire everything and assert nothing found
|
// Expire everything and assert nothing found
|
||||||
Time.setOffset(6000000);
|
Time.setOffset(7000000);
|
||||||
|
|
||||||
currentSession.sessions().removeExpired(realm);
|
currentSession.sessions().removeExpired(realm);
|
||||||
persister.removeExpired(realm);
|
persister.removeExpired(realm);
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
@ -57,6 +58,7 @@ import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
|
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -64,6 +66,9 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
|
||||||
@AuthServerContainerExclude(AuthServer.REMOTE)
|
@AuthServerContainerExclude(AuthServer.REMOTE)
|
||||||
public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||||
|
|
||||||
public static void setupRealm(KeycloakSession session){
|
public static void setupRealm(KeycloakSession session){
|
||||||
RealmModel realm = session.realms().getRealmByName("test");
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
UserModel user1 = session.users().addUser(realm, "user1");
|
UserModel user1 = session.users().addUser(realm, "user1");
|
||||||
|
@ -469,6 +474,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals(userSession, clientSession.getUserSession());
|
assertEquals(userSession, clientSession.getUserSession());
|
||||||
Time.setOffset(-(realm.getSsoSessionIdleTimeout() * 2));
|
Time.setOffset(-(realm.getSsoSessionIdleTimeout() * 2));
|
||||||
userSession.setLastSessionRefresh(Time.currentTime());
|
userSession.setLastSessionRefresh(Time.currentTime());
|
||||||
|
clientSession.setTimestamp(Time.currentTime());
|
||||||
validUserSessions.add(userSession.getId());
|
validUserSessions.add(userSession.getId());
|
||||||
validClientSessions.add(clientSession.getId());
|
validClientSessions.add(clientSession.getId());
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.junit.rules.ExternalResource;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.arquillian.ContainerInfo;
|
||||||
|
import org.keycloak.testsuite.arquillian.CrossDCTestEnricher;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.arquillian.CrossDCTestEnricher.forAllBackendNodesStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class InfinispanTestTimeServiceRule extends ExternalResource {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(InfinispanTestTimeServiceRule.class);
|
||||||
|
|
||||||
|
private final AbstractKeycloakTest test;
|
||||||
|
|
||||||
|
public InfinispanTestTimeServiceRule(AbstractKeycloakTest test) {
|
||||||
|
this.test = test;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void before() throws Throwable {
|
||||||
|
if (!this.test.getTestContext().getSuiteContext().isAuthServerCrossDc()) {
|
||||||
|
// No cross-dc environment
|
||||||
|
test.getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
|
} else {
|
||||||
|
AtomicInteger count = new AtomicInteger(0);
|
||||||
|
// Cross-dc environment - Set on all started nodes
|
||||||
|
forAllBackendNodesStream()
|
||||||
|
.filter(ContainerInfo::isStarted)
|
||||||
|
.map(CrossDCTestEnricher.getBackendTestingClients()::get)
|
||||||
|
.forEach(testingClient -> {
|
||||||
|
testingClient.testing().setTestingInfinispanTimeService();
|
||||||
|
count.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
log.infof("Totally set infinispanTimeService rule in %d servers", count.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void after() {
|
||||||
|
if (!this.test.getTestContext().getSuiteContext().isAuthServerCrossDc()) {
|
||||||
|
// No cross-dc environment
|
||||||
|
test.getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
} else {
|
||||||
|
// Cross-dc environment - Revert on all started nodes
|
||||||
|
forAllBackendNodesStream()
|
||||||
|
.filter(ContainerInfo::isStarted)
|
||||||
|
.map(CrossDCTestEnricher.getBackendTestingClients()::get)
|
||||||
|
.forEach(testingClient -> testingClient.testing().revertTestingInfinispanTimeService());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue