diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl index f800ae2359..7f18d0df34 100755 --- a/distribution/demo-dist/src/main/xslt/standalone.xsl +++ b/distribution/demo-dist/src/main/xslt/standalone.xsl @@ -91,6 +91,8 @@ + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli index 4dd4e56f65..9bfd8dc97f 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli @@ -398,4 +398,16 @@ if (outcome == success) of /profile=$clusteredProfile/subsystem=jsf/:read-resour end-try end-if +if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:read-resource + echo Adding distributed-cache=offlineClientSessions to keycloak cache container... + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:add(mode=SYNC,owners=1) + echo +end-if + +if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:read-resource + echo Adding distributed-cache=clientSessions to keycloak cache container... + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:add(mode=SYNC,owners=1) + echo +end-if + echo *** End Migration of /profile=$clusteredProfile *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli index 100808a172..1632413568 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli @@ -361,4 +361,16 @@ if (outcome == success) of /profile=$standaloneProfile/subsystem=jsf/:read-resou end-try end-if +if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:read-resource + echo Adding local-cache=clientSessions to keycloak cache container... + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:add(indexing=NONE,start=LAZY) + echo +end-if + +if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:read-resource + echo Adding local-cache=offlineClientSessions to keycloak cache container... + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:add(indexing=NONE,start=LAZY) + echo +end-if + echo *** End Migration of /profile=$standaloneProfile *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli index 50e51e6518..918ed3510f 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli @@ -382,4 +382,16 @@ if (outcome == success) of /subsystem=jsf/:read-resource echo end-if +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:read-resource + echo Adding distributed-cache=clientSessions to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:add(mode=SYNC,owners=1) + echo +end-if + +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:read-resource + echo Adding distributed-cache=offlineClientSessions to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:add(mode=SYNC,owners=1) + echo +end-if + echo *** End Migration *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli index e348149eb1..c9d0cabcb3 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli @@ -350,4 +350,16 @@ if (outcome == success) of /subsystem=jsf/:read-resource echo end-if +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:read-resource + echo Adding local-cache=offlineClientSessions to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:add(indexing=NONE,start=LAZY) + echo +end-if + +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:read-resource + echo Adding local-cache=clientSessions to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:add(indexing=NONE,start=LAZY) + echo +end-if + echo *** End Migration *** \ No newline at end of file diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli index 6bb11d57d4..d4b02f8632 100644 --- a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli +++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli @@ -9,6 +9,8 @@ embed-server --server-config=standalone-ha.xml /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1") +/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add(mode="SYNC",owners="1") +/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add() /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=10000,strategy=LRU) diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md index c24a890218..fa69f2d42a 100644 --- a/misc/CrossDataCenter.md +++ b/misc/CrossDataCenter.md @@ -188,7 +188,8 @@ Keycloak servers setup ``` -3.6) Same for `offlineSessions`, `loginFailures`, and `actionTokens` caches (the only difference from `sessions` cache is that `cache` property value are different): +3.6) Same for `offlineSessions`, `clientSessions`, `offlineClientSessions`, `loginFailures`, and `actionTokens` caches (the only difference +from `sessions` cache is that `cache` property value are different): ```xml @@ -198,6 +199,20 @@ Keycloak servers setup + + + true + org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory + + + + + + true + org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory + + + true diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index c657e44267..572c5f0a17 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -245,18 +245,34 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon if (jdgEnabled) { sessionConfigBuilder = new ConfigurationBuilder(); sessionConfigBuilder.read(sessionCacheConfigurationBase); - configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.SESSION_CACHE_NAME, true); + configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, true); } Configuration sessionCacheConfiguration = sessionConfigBuilder.build(); - cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration); + cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, sessionCacheConfiguration); if (jdgEnabled) { sessionConfigBuilder = new ConfigurationBuilder(); sessionConfigBuilder.read(sessionCacheConfigurationBase); - configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true); + configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true); } sessionCacheConfiguration = sessionConfigBuilder.build(); - cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration); + cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, sessionCacheConfiguration); + + if (jdgEnabled) { + sessionConfigBuilder = new ConfigurationBuilder(); + sessionConfigBuilder.read(sessionCacheConfigurationBase); + configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, true); + } + sessionCacheConfiguration = sessionConfigBuilder.build(); + cacheManager.defineConfiguration(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, sessionCacheConfiguration); + + if (jdgEnabled) { + sessionConfigBuilder = new ConfigurationBuilder(); + sessionConfigBuilder.read(sessionCacheConfigurationBase); + configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, true); + } + sessionCacheConfiguration = sessionConfigBuilder.build(); + cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, sessionCacheConfiguration); if (jdgEnabled) { sessionConfigBuilder = new ConfigurationBuilder(); @@ -269,8 +285,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase); // Retrieve caches to enforce rebalance - cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true); - cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true); diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index 9c3d437de9..00f60a74a6 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -33,8 +33,10 @@ public interface InfinispanConnectionProvider extends Provider { String USER_REVISIONS_CACHE_NAME = "userRevisions"; int USER_REVISIONS_CACHE_DEFAULT_MAX = 100000; - String SESSION_CACHE_NAME = "sessions"; - String OFFLINE_SESSION_CACHE_NAME = "offlineSessions"; + String USER_SESSION_CACHE_NAME = "sessions"; + String CLIENT_SESSION_CACHE_NAME = "clientSessions"; + String OFFLINE_USER_SESSION_CACHE_NAME = "offlineSessions"; + String OFFLINE_CLIENT_SESSION_CACHE_NAME = "offlineClientSessions"; String LOGIN_FAILURE_CACHE_NAME = "loginFailures"; String AUTHENTICATION_SESSIONS_CACHE_NAME = "authenticationSessions"; String WORK_CACHE_NAME = "work"; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java index a786084073..736e7568cb 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java @@ -28,10 +28,15 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; -import org.keycloak.models.sessions.infinispan.changes.UserSessionClientSessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.ClientSessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus; +import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import java.util.UUID; /** * @author Marek Posolda @@ -40,56 +45,48 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes private AuthenticatedClientSessionEntity entity; private final ClientModel client; - private final InfinispanUserSessionProvider provider; - private final InfinispanChangelogBasedTransaction updateTx; - private UserSessionAdapter userSession; + private final InfinispanChangelogBasedTransaction userSessionUpdateTx; + private final InfinispanChangelogBasedTransaction clientSessionUpdateTx; + private UserSessionModel userSession; + + public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, + UserSessionModel userSession, + InfinispanChangelogBasedTransaction userSessionUpdateTx, + InfinispanChangelogBasedTransaction clientSessionUpdateTx) { + if (userSession == null) { + throw new NullPointerException("userSession must not be null"); + } - public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, - InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx) { - this.provider = provider; this.entity = entity; - this.client = client; - this.updateTx = updateTx; this.userSession = userSession; + this.client = client; + this.userSessionUpdateTx = userSessionUpdateTx; + this.clientSessionUpdateTx = clientSessionUpdateTx; } private void update(UserSessionUpdateTask task) { - updateTx.addTask(userSession.getId(), task); + userSessionUpdateTx.addTask(userSession.getId(), task); } + private void update(ClientSessionUpdateTask task) { + clientSessionUpdateTx.addTask(entity.getId(), task); + } + /** + * Detaches the client session from its user session. + *

+ * This method does not delete the client session from user session records, it only removes the client session. + * The list of client sessions within user session is updated lazily for performance reasons. + */ @Override - public void setUserSession(UserSessionModel userSession) { - String clientUUID = client.getId(); + public void detachFromUserSession() { + // Intentionally do not remove the clientUUID from the user session, invalid session is handled + // as nonexistent in org.keycloak.models.sessions.infinispan.UserSessionAdapter.getAuthenticatedClientSessions() + this.userSession = null; - // Dettach userSession - if (userSession == null) { - UserSessionUpdateTask task = new UserSessionUpdateTask() { + SessionUpdateTask removeTask = Tasks.removeSync(); - @Override - public void runUpdate(UserSessionEntity sessionEntity) { - sessionEntity.getAuthenticatedClientSessions().remove(clientUUID); - } - - }; - update(task); - this.userSession = null; - } else { - this.userSession = (UserSessionAdapter) userSession; - UserSessionUpdateTask task = new UserSessionUpdateTask() { - - @Override - public void runUpdate(UserSessionEntity sessionEntity) { - AuthenticatedClientSessionEntity current = sessionEntity.getAuthenticatedClientSessions().putIfAbsent(clientUUID, entity); - if (current != null) { - // It may happen when 2 concurrent HTTP requests trying SSO login against same client - entity = current; - } - } - - }; - update(task); - } + clientSessionUpdateTx.addTask(entity.getId(), removeTask); } @Override @@ -104,10 +101,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setRedirectUri(String uri) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setRedirectUri(uri); } @@ -138,19 +135,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setTimestamp(int timestamp) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setTimestamp(timestamp); } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - // We usually update lastSessionRefresh at the same time. That would handle it. - return CrossDCMessageStatus.NOT_NEEDED; - } - }; update(task); @@ -163,19 +153,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount); } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - // We usually update lastSessionRefresh at the same time. That would handle it. - return CrossDCMessageStatus.NOT_NEEDED; - } - }; update(task); @@ -188,19 +171,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setCurrentRefreshToken(String currentRefreshToken) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setCurrentRefreshToken(currentRefreshToken); } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - // We usually update lastSessionRefresh at the same time. That would handle it. - return CrossDCMessageStatus.NOT_NEEDED; - } - }; update(task); @@ -213,10 +189,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setAction(String action) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setAction(action); } @@ -232,10 +208,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setProtocol(String method) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setAuthMethod(method); } @@ -251,10 +227,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setRoles(Set roles) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setRoles(roles); // TODO not thread-safe. But we will remove setRoles anyway...? } @@ -270,10 +246,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setProtocolMappers(Set protocolMappers) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.setProtocolMappers(protocolMappers); // TODO not thread-safe. But we will remove setProtocolMappers anyway...? } @@ -289,10 +265,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setNote(String name, String value) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.getNotes().put(name, value); } @@ -303,10 +279,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void removeNote(String name) { - UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { @Override - protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + public void runUpdate(AuthenticatedClientSessionEntity entity) { entity.getNotes().remove(name); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 8b6df90c3d..2c0411ec48 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -23,7 +23,6 @@ import org.infinispan.context.Flag; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; import org.keycloak.common.util.Time; -import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -33,12 +32,16 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore; import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; @@ -58,8 +61,10 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -75,10 +80,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected final Cache> sessionCache; protected final Cache> offlineSessionCache; + protected final Cache> clientSessionCache; + protected final Cache> offlineClientSessionCache; protected final Cache> loginFailureCache; protected final InfinispanChangelogBasedTransaction sessionTx; protected final InfinispanChangelogBasedTransaction offlineSessionTx; + protected final InfinispanChangelogBasedTransaction clientSessionTx; + protected final InfinispanChangelogBasedTransaction offlineClientSessionTx; protected final InfinispanChangelogBasedTransaction loginFailuresTx; protected final SessionEventsSenderTransaction clusterEventsSenderTx; @@ -92,17 +101,23 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { LastSessionRefreshStore offlineLastSessionRefreshStore, Cache> sessionCache, Cache> offlineSessionCache, + Cache> clientSessionCache, + Cache> offlineClientSessionCache, Cache> loginFailureCache) { this.session = session; this.sessionCache = sessionCache; + this.clientSessionCache = clientSessionCache; this.offlineSessionCache = offlineSessionCache; + this.offlineClientSessionCache = offlineClientSessionCache; this.loginFailureCache = loginFailureCache; - this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCache, remoteCacheInvoker); - this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionCache, remoteCacheInvoker); + this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker); + this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker); + this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker); + this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker); - this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, loginFailureCache, remoteCacheInvoker); + this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker); this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); @@ -112,6 +127,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); session.getTransactionManager().enlistAfterCompletion(sessionTx); session.getTransactionManager().enlistAfterCompletion(offlineSessionTx); + session.getTransactionManager().enlistAfterCompletion(clientSessionTx); + session.getTransactionManager().enlistAfterCompletion(offlineClientSessionTx); session.getTransactionManager().enlistAfterCompletion(loginFailuresTx); } @@ -123,6 +140,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return offline ? offlineSessionTx : sessionTx; } + protected Cache> getClientSessionCache(boolean offline) { + return offline ? offlineClientSessionCache : clientSessionCache; + } + + protected InfinispanChangelogBasedTransaction getClientSessionTransaction(boolean offline) { + return offline ? offlineClientSessionTx : clientSessionTx; + } + protected LastSessionRefreshStore getLastSessionRefreshStore() { return lastSessionRefreshStore; } @@ -134,10 +159,20 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); + entity.setRealmId(realm.getId()); + final UUID clientSessionId = entity.getId(); + + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(false); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(false); + AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, + userSessionUpdateTx, clientSessionUpdateTx); + + SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); + clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity); + + SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId); + userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask); - InfinispanChangelogBasedTransaction updateTx = getTransaction(false); - AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx); - adapter.setUserSession(userSession); return adapter; } @@ -147,25 +182,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setId(id); updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); - SessionUpdateTask createSessionTask = new SessionUpdateTask() { - - @Override - public void runUpdate(UserSessionEntity session) { - - } - - @Override - public CacheOperation getOperation(UserSessionEntity session) { - return CacheOperation.ADD_IF_ABSENT; - } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - return CrossDCMessageStatus.SYNC; - } - - }; - + SessionUpdateTask createSessionTask = Tasks.addIfAbsentSync(); sessionTx.addTask(id, createSessionTask, entity); return wrap(realm, entity, false); @@ -228,6 +245,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return resultSessions; } + @Override + public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline) { + AuthenticatedClientSessionEntity entity = getClientSessionEntity(clientSessionId, offline); + return wrap(userSession, client, entity, offline); + } + + private AuthenticatedClientSessionEntity getClientSessionEntity(UUID id, boolean offline) { + InfinispanChangelogBasedTransaction tx = getClientSessionTransaction(offline); + SessionEntityWrapper entityWrapper = tx.get(id); + return entityWrapper == null ? null : entityWrapper.getEntity(); + } + + @Override public List getUserSessions(final RealmModel realm, UserModel user) { return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), false); @@ -256,12 +286,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { Cache> cache = getCache(offline); - cache = CacheDecorators.skipCacheLoaders(cache); + Cache> clientSessionCache = getClientSessionCache(offline); + Cache> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache); + + final String clientUuid = client.getId(); + Stream stream = cache.entrySet().stream() - .filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) + .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid)) .map(Mappers.userSessionEntity()) + // Filter out client sessions that have been invalidated in the meantime + .filter(userSession -> { + final UUID clientSessionId = userSession.getAuthenticatedClientSessions().get(clientUuid); + return clientSessionId != null && clientSessionCacheDecorated.containsKey(clientSessionId); + }) .sorted(Comparators.userSessionLastSessionRefresh()); if (firstResult > 0) { @@ -356,8 +395,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { Cache> cache = getCache(offline); cache = CacheDecorators.skipCacheLoaders(cache); + Cache> clientSessionCache = getClientSessionCache(offline); + Cache> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache); + + final String clientUuid = client.getId(); + return cache.entrySet().stream() - .filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) + .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid)) + // Filter out client sessions that have been invalidated in the meantime + .map(Mappers.userSessionEntity()) + .filter(userSession -> { + final UUID clientSessionId = userSession.getAuthenticatedClientSessions().get(clientUuid); + return clientSessionId != null && clientSessionCacheDecorated.containsKey(clientSessionId); + }) .count(); } @@ -402,28 +452,38 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { // Each cluster node cleanups just local sessions, which are those owned by itself (+ few more taking l1 cache into account) Cache> localCache = CacheDecorators.localCache(sessionCache); + Cache> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache); Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + final AtomicInteger userSessionsSize = 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)) - .map(Mappers.sessionId()) - .forEach(new Consumer() { + .map(Mappers.userSessionEntity()) + .forEach(new Consumer() { @Override - public void accept(String sessionId) { - Future future = localCache.removeAsync(sessionId); + public void accept(UserSessionEntity userSessionEntity) { + userSessionsSize.incrementAndGet(); + + Future future = localCache.removeAsync(userSessionEntity.getId()); futures.addTask(future); + + userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { + Future f = localClientSessionCache.removeAsync(clientSessionId); + futures.addTask(f); + }); } }); futures.waitForAllToFinish(); - log.debugf("Removed %d expired user sessions for realm '%s'", futures.size(), realm.getName()); + log.debugf("Removed %d expired user sessions for realm '%s'", userSessionsSize.get(), realm.getName()); } private void removeExpiredOfflineUserSessions(RealmModel realm) { @@ -432,6 +492,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) Cache> localCache = CacheDecorators.localCache(offlineSessionCache); + Cache> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache); UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline); @@ -439,6 +500,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + final AtomicInteger userSessionsSize = new AtomicInteger(); + // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate localCacheStoreIgnore .entrySet() @@ -449,8 +512,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void accept(UserSessionEntity userSessionEntity) { + userSessionsSize.incrementAndGet(); + Future future = localCache.removeAsync(userSessionEntity.getId()); futures.addTask(future); + userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { + Future f = localClientSessionCache.removeAsync(clientSessionId); + futures.addTask(f); + }); // TODO:mposolda can be likely optimized to delete all expired at one step persister.removeUserSession( userSessionEntity.getId(), true); @@ -464,7 +533,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { futures.waitForAllToFinish(); - log.debugf("Removed %d expired offline user sessions for realm '%s'", futures.size(), realm.getName()); + log.debugf("Removed %d expired offline user sessions for realm '%s'", userSessionsSize.get(), realm.getName()); } @Override @@ -484,28 +553,39 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { Cache> cache = getCache(offline); Cache> localCache = CacheDecorators.localCache(cache); + Cache> clientSessionCache = getClientSessionCache(offline); + Cache> localClientSessionCache = CacheDecorators.localCache(clientSessionCache); Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + final AtomicInteger userSessionsSize = new AtomicInteger(); + localCacheStoreIgnore .entrySet() .stream() .filter(SessionPredicate.create(realmId)) - .map(Mappers.sessionId()) - .forEach(new Consumer() { + .map(Mappers.userSessionEntity()) + .forEach(new Consumer() { @Override - public void accept(String sessionId) { + public void accept(UserSessionEntity userSessionEntity) { + userSessionsSize.incrementAndGet(); + // Remove session from remoteCache too. Use removeAsync for better perf - Future future = localCache.removeAsync(sessionId); + Future future = localCache.removeAsync(userSessionEntity.getId()); futures.addTask(future); + userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { + Future f = localClientSessionCache.removeAsync(clientSessionId); + futures.addTask(f); + }); } }); + futures.waitForAllToFinish(); - log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) futures.size(), realmId, offline); + log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) userSessionsSize.get(), realmId, offline); } @Override @@ -528,25 +608,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setRealmId(realm.getId()); entity.setUserId(userId); - SessionUpdateTask createLoginFailureTask = new SessionUpdateTask() { - - @Override - public void runUpdate(LoginFailureEntity session) { - - } - - @Override - public CacheOperation getOperation(LoginFailureEntity session) { - return CacheOperation.ADD_IF_ABSENT; - } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - return CrossDCMessageStatus.SYNC; - } - - }; - + SessionUpdateTask createLoginFailureTask = Tasks.addIfAbsentSync(); loginFailuresTx.addTask(key, createLoginFailureTask, entity); return wrap(key, entity); @@ -554,25 +616,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void removeUserLoginFailure(RealmModel realm, String userId) { - SessionUpdateTask removeTask = new SessionUpdateTask() { - - @Override - public void runUpdate(LoginFailureEntity entity) { - - } - - @Override - public CacheOperation getOperation(LoginFailureEntity entity) { - return CacheOperation.REMOVE; - } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - return CrossDCMessageStatus.SYNC; - } - - }; - + SessionUpdateTask removeTask = Tasks.removeSync(); loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask); } @@ -648,28 +692,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) { - InfinispanChangelogBasedTransaction tx = getTransaction(offline); - - SessionUpdateTask removeTask = new SessionUpdateTask() { - - @Override - public void runUpdate(UserSessionEntity entity) { - - } - - @Override - public CacheOperation getOperation(UserSessionEntity entity) { - return CacheOperation.REMOVE; - } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - return CrossDCMessageStatus.SYNC; - } - - }; - - tx.addTask(sessionEntity.getId(), removeTask); + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + sessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync())); + SessionUpdateTask removeTask = Tasks.removeSync(); + userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask); } InfinispanChangelogBasedTransaction getLoginFailuresTx() { @@ -677,8 +704,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { - InfinispanChangelogBasedTransaction tx = getTransaction(offline); - return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null; + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + return entity != null ? new UserSessionAdapter(session, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline) : null; + } + + AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) { + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + return entity != null ? new AuthenticatedClientSessionAdapter(entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx) : null; } UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { @@ -726,7 +760,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); - AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, getTransaction(true)); + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(true); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(true); + AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx); // update timestamp to current time offlineClientSession.setTimestamp(Time.currentTime()); @@ -776,7 +812,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setIpAddress(userSession.getIpAddress()); entity.setLoginUsername(userSession.getLoginUsername()); entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes()); - entity.setAuthenticatedClientSessions(new ConcurrentHashMap<>()); + entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore()); entity.setRememberMe(userSession.isRememberMe()); entity.setState(userSession.getState()); entity.setUser(userSession.getUser().getId()); @@ -784,35 +820,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setStarted(userSession.getStarted()); entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); - InfinispanChangelogBasedTransaction tx = getTransaction(offline); - - SessionUpdateTask importTask = new SessionUpdateTask() { - - @Override - public void runUpdate(UserSessionEntity session) { - - } - - @Override - public CacheOperation getOperation(UserSessionEntity session) { - return CacheOperation.ADD_IF_ABSENT; - } - - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - return CrossDCMessageStatus.SYNC; - } - - }; - tx.addTask(userSession.getId(), importTask, entity); + SessionUpdateTask importTask = Tasks.addIfAbsentSync(); + userSessionUpdateTx.addTask(userSession.getId(), importTask, entity); UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline); // Handle client sessions if (importAuthenticatedClientSessions) { for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { - importClientSession(importedSession, clientSession, tx); + importClientSession(importedSession, clientSession, userSessionUpdateTx, clientSessionUpdateTx); } } @@ -820,9 +839,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } - private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession, - InfinispanChangelogBasedTransaction updateTx) { + private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession, + InfinispanChangelogBasedTransaction userSessionUpdateTx, + InfinispanChangelogBasedTransaction clientSessionUpdateTx) { AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); + entity.setRealmId(sessionToImportInto.getRealm().getId()); + final UUID clientSessionId = entity.getId(); entity.setAction(clientSession.getAction()); entity.setAuthMethod(clientSession.getProtocol()); @@ -833,33 +855,43 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setRoles(clientSession.getRoles()); entity.setTimestamp(clientSession.getTimestamp()); + SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); + clientSessionUpdateTx.addTask(entity.getId(), createClientSessionTask, entity); - Map clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions(); + AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); + clientSessions.put(clientSession.getClient().getId(), clientSessionId); - clientSessions.put(clientSession.getClient().getId(), entity); + SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId); + userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask); - SessionUpdateTask importTask = new SessionUpdateTask() { + return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx); + } - @Override - public void runUpdate(UserSessionEntity session) { - Map clientSessions = session.getAuthenticatedClientSessions(); - clientSessions.put(clientSession.getClient().getId(), entity); - } + private static class RegisterClientSessionTask implements SessionUpdateTask { - @Override - public CacheOperation getOperation(UserSessionEntity session) { - return CacheOperation.REPLACE; - } + private final String clientUuid; + private final UUID clientSessionId; - @Override - public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { - return CrossDCMessageStatus.SYNC; - } + public RegisterClientSessionTask(String clientUuid, UUID clientSessionId) { + this.clientUuid = clientUuid; + this.clientSessionId = clientSessionId; + } - }; - updateTx.addTask(importedUserSession.getId(), importTask); + @Override + public void runUpdate(UserSessionEntity session) { + AuthenticatedClientSessionStore clientSessions = session.getAuthenticatedClientSessions(); + clientSessions.put(clientUuid, clientSessionId); + } - return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, updateTx); + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.REPLACE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index ccc9b84943..ae2b3d46cb 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -37,6 +37,7 @@ import org.keycloak.models.sessions.infinispan.initializer.CacheInitializer; import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheInitializer; import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; @@ -58,6 +59,7 @@ import org.keycloak.provider.ProviderEventListener; import java.io.Serializable; import java.util.Set; +import java.util.UUID; public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory { @@ -82,11 +84,14 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider @Override public InfinispanUserSessionProvider create(KeycloakSession session) { InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); - Cache> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); - Cache> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + Cache> cache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); + Cache> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); + Cache> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); + Cache> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); Cache> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); - return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures); + return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, + cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, loginFailures); } @Override @@ -205,7 +210,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class); - Cache> sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> { return realm.getSsoSessionIdleTimeout() * 1000; }); @@ -214,8 +219,12 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider lastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false); } + Cache> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); + checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> { + return realm.getSsoSessionIdleTimeout() * 1000; + }); - Cache> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + Cache> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> { return realm.getOfflineSessionIdleTimeout() * 1000; }); @@ -224,8 +233,13 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true); } + Cache> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); + checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> { + return realm.getOfflineSessionIdleTimeout() * 1000; + }); + Cache> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); - boolean loginFailuresRemoteCache = checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> { + checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> { return realm.getMaxDeltaTimeSeconds() * 1000; }); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index 635d4f7a0c..de825573fe 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -26,15 +26,22 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshChecker; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** * @author Stian Thorgersen @@ -45,7 +52,9 @@ public class UserSessionAdapter implements UserSessionModel { private final InfinispanUserSessionProvider provider; - private final InfinispanChangelogBasedTransaction updateTx; + private final InfinispanChangelogBasedTransaction userSessionUpdateTx; + + private final InfinispanChangelogBasedTransaction clientSessionUpdateTx; private final RealmModel realm; @@ -53,11 +62,14 @@ public class UserSessionAdapter implements UserSessionModel { private final boolean offline; - public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx, RealmModel realm, - UserSessionEntity entity, boolean offline) { + public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, + InfinispanChangelogBasedTransaction userSessionUpdateTx, + InfinispanChangelogBasedTransaction clientSessionUpdateTx, + RealmModel realm, UserSessionEntity entity, boolean offline) { this.session = session; this.provider = provider; - this.updateTx = updateTx; + this.userSessionUpdateTx = userSessionUpdateTx; + this.clientSessionUpdateTx = clientSessionUpdateTx; this.realm = realm; this.entity = entity; this.offline = offline; @@ -65,17 +77,20 @@ public class UserSessionAdapter implements UserSessionModel { @Override public Map getAuthenticatedClientSessions() { - Map clientSessionEntities = entity.getAuthenticatedClientSessions(); + AuthenticatedClientSessionStore clientSessionEntities = entity.getAuthenticatedClientSessions(); Map result = new HashMap<>(); List removedClientUUIDS = new LinkedList<>(); if (clientSessionEntities != null) { - clientSessionEntities.forEach((String key, AuthenticatedClientSessionEntity value) -> { + clientSessionEntities.forEach((String key, UUID value) -> { // Check if client still exists ClientModel client = realm.getClientById(key); if (client != null) { - result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, updateTx)); + final AuthenticatedClientSessionAdapter clientSession = provider.getClientSession(this, client, value, offline); + if (clientSession != null) { + result.put(key, clientSession); + } } else { removedClientUUIDS.add(key); } @@ -88,20 +103,53 @@ public class UserSessionAdapter implements UserSessionModel { } @Override - public void removeAuthenticatedClientSessions(Iterable removedClientUUIDS) { - if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) { + public AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) { + AuthenticatedClientSessionStore clientSessionEntities = entity.getAuthenticatedClientSessions(); + final UUID clientSessionId = clientSessionEntities.get(clientUUID); + + if (clientSessionId == null) { + return null; + } + + ClientModel client = realm.getClientById(clientUUID); + + if (client != null) { + return provider.getClientSession(this, client, clientSessionId, offline); + } + + removeAuthenticatedClientSessions(Collections.singleton(clientUUID)); + return null; + } + + private static final int MINIMUM_INACTIVE_CLIENT_SESSIONS_TO_CLEANUP = 5; + + @Override + public void removeAuthenticatedClientSessions(Collection removedClientUUIDS) { + if (removedClientUUIDS == null || ! removedClientUUIDS.isEmpty()) { return; } - // Update user session - UserSessionUpdateTask task = new UserSessionUpdateTask() { - @Override - public void runUpdate(UserSessionEntity entity) { - removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove); - } - }; + // Performance: do not remove the clientUUIDs from the user session until there is enough of them; + // an invalid session is handled as nonexistent in UserSessionAdapter.getAuthenticatedClientSessions() + if (removedClientUUIDS.size() >= MINIMUM_INACTIVE_CLIENT_SESSIONS_TO_CLEANUP) { + // Update user session + UserSessionUpdateTask task = new UserSessionUpdateTask() { + @Override + public void runUpdate(UserSessionEntity entity) { + removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove); + } + }; + update(task); + } - update(task); + // do not iterate the removedClientUUIDS and remove the clientSession directly as the addTask can manipulate + // the collection being iterated, and that can lead to unpredictable behaviour (e.g. NPE) + List clientSessionUuids = removedClientUUIDS.stream() + .map(entity.getAuthenticatedClientSessions()::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + clientSessionUuids.forEach(clientSessionId -> this.clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync())); } public String getId() { @@ -276,7 +324,7 @@ public class UserSessionAdapter implements UserSessionModel { } void update(UserSessionUpdateTask task) { - updateTx.addTask(getId(), task); + userSessionUpdateTx.addTask(getId(), task); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionUpdateTask.java new file mode 100644 index 0000000000..f587c45142 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionUpdateTask.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; + +/** + * @author Marek Posolda + */ +public abstract class ClientSessionUpdateTask implements SessionUpdateTask { + + @Override + public CacheOperation getOperation(AuthenticatedClientSessionEntity session) { + return CacheOperation.REPLACE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java index 694c41e461..2a0b4362bc 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java @@ -45,9 +45,9 @@ public class InfinispanChangelogBasedTransaction ext private final Map> updates = new HashMap<>(); - public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache> cache, RemoteCacheInvoker remoteCacheInvoker) { + public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache> cache, RemoteCacheInvoker remoteCacheInvoker) { this.kcSession = kcSession; - this.cacheName = cacheName; + this.cacheName = cache.getName(); this.cache = cache; this.remoteCacheInvoker = remoteCacheInvoker; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/Tasks.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/Tasks.java new file mode 100644 index 0000000000..214e2e9256 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/Tasks.java @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.sessions.infinispan.changes; + +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * + * @author hmlnarik + */ +public class Tasks { + + private static final SessionUpdateTask ADD_IF_ABSENT_SYNC = new SessionUpdateTask() { + @Override + public void runUpdate(SessionEntity entity) { + } + + @Override + public CacheOperation getOperation(SessionEntity entity) { + return CacheOperation.ADD_IF_ABSENT; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + }; + + private static final SessionUpdateTask REMOVE_SYNC = new SessionUpdateTask() { + @Override + public void runUpdate(SessionEntity entity) { + } + + @Override + public CacheOperation getOperation(SessionEntity entity) { + return CacheOperation.REMOVE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + }; + + /** + * Returns a typed task of type {@link CacheOperation#ADD_IF_ABSENT} that does no other update. This operation has DC message + * status {@link CrossDCMessageStatus#SYNC}. + * @param + * @return + */ + public static SessionUpdateTask addIfAbsentSync() { + return (SessionUpdateTask) ADD_IF_ABSENT_SYNC; + } + + /** + * Returns a typed task of type {@link CacheOperation#REMOVE} that does no other update. This operation has DC message + * status {@link CrossDCMessageStatus#SYNC}. + * @param + * @return + */ + public static SessionUpdateTask removeSync() { + return (SessionUpdateTask) REMOVE_SYNC; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java deleted file mode 100644 index 56e0403143..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.sessions.infinispan.changes; - -import org.jboss.logging.Logger; -import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; - -/** - * Task for create or update AuthenticatedClientSessionEntity within userSession - * - * @author Marek Posolda - */ -public abstract class UserSessionClientSessionUpdateTask extends UserSessionUpdateTask { - - public static final Logger logger = Logger.getLogger(UserSessionClientSessionUpdateTask.class); - - private final String clientUUID; - - public UserSessionClientSessionUpdateTask(String clientUUID) { - this.clientUUID = clientUUID; - } - - @Override - public void runUpdate(UserSessionEntity userSession) { - AuthenticatedClientSessionEntity clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID); - if (clientSession == null) { - logger.warnf("Not found authenticated client session entity for client %s in userSession %s", clientUUID, userSession.getId()); - return; - } - - runClientSessionUpdate(clientSession); - } - - protected abstract void runClientSessionUpdate(AuthenticatedClientSessionEntity entity); -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java index b8a62239d7..898648a7c7 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java @@ -20,7 +20,6 @@ package org.keycloak.models.sessions.infinispan.entities; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; -import java.io.Serializable; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -29,13 +28,14 @@ import org.infinispan.commons.marshall.Externalizer; import org.infinispan.commons.marshall.MarshallUtil; import org.infinispan.commons.marshall.SerializeWith; import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; +import java.util.UUID; /** * * @author Marek Posolda */ @SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class) -public class AuthenticatedClientSessionEntity implements Serializable { +public class AuthenticatedClientSessionEntity extends SessionEntity { private String authMethod; private String redirectUri; @@ -49,6 +49,16 @@ public class AuthenticatedClientSessionEntity implements Serializable { private String currentRefreshToken; private int currentRefreshTokenUseCount; + private final UUID id; + + private AuthenticatedClientSessionEntity(UUID id) { + this.id = id; + } + + public AuthenticatedClientSessionEntity() { + this.id = UUID.randomUUID(); + } + public String getAuthMethod() { return authMethod; } @@ -121,10 +131,16 @@ public class AuthenticatedClientSessionEntity implements Serializable { this.currentRefreshTokenUseCount = currentRefreshTokenUseCount; } + public UUID getId() { + return id; + } + public static class ExternalizerImpl implements Externalizer { @Override public void writeObject(ObjectOutput output, AuthenticatedClientSessionEntity session) throws IOException { + MarshallUtil.marshallUUID(session.id, output, false); + MarshallUtil.marshallString(session.getRealmId(), output); MarshallUtil.marshallString(session.getAuthMethod(), output); MarshallUtil.marshallString(session.getRedirectUri(), output); MarshallUtil.marshallInt(output, session.getTimestamp()); @@ -143,7 +159,9 @@ public class AuthenticatedClientSessionEntity implements Serializable { @Override public AuthenticatedClientSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException { - AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity(); + AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity(MarshallUtil.unmarshallUUID(input, false)); + + sessionEntity.setRealmId(MarshallUtil.unmarshallString(input)); sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input)); sessionEntity.setRedirectUri(MarshallUtil.unmarshallString(input)); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionStore.java new file mode 100644 index 0000000000..ebf946e295 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionStore.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.sessions.infinispan.entities; + +import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.SerializeWith; + +/** + * + * @author hmlnarik + */ +@SerializeWith(AuthenticatedClientSessionStore.ExternalizerImpl.class) +public class AuthenticatedClientSessionStore { + + /** + * Maps client UUID to client session ID. + */ + private final ConcurrentHashMap authenticatedClientSessionIds; + + public AuthenticatedClientSessionStore() { + authenticatedClientSessionIds = new ConcurrentHashMap<>(); + } + + private AuthenticatedClientSessionStore(ConcurrentHashMap authenticatedClientSessionIds) { + this.authenticatedClientSessionIds = authenticatedClientSessionIds; + } + + public void clear() { + authenticatedClientSessionIds.clear(); + } + + public boolean containsKey(String key) { + return authenticatedClientSessionIds.containsKey(key); + } + + public void forEach(BiConsumer action) { + authenticatedClientSessionIds.forEach(action); + } + + public UUID get(String key) { + return authenticatedClientSessionIds.get(key); + } + + public Set keySet() { + return authenticatedClientSessionIds.keySet(); + } + + public UUID put(String key, UUID value) { + return authenticatedClientSessionIds.put(key, value); + } + + public UUID remove(String clientUUID) { + return authenticatedClientSessionIds.remove(clientUUID); + } + + public int size() { + return authenticatedClientSessionIds.size(); + } + + @Override + public String toString() { + return this.authenticatedClientSessionIds.toString(); + } + + public static class ExternalizerImpl implements Externalizer { + + private static final int VERSION_1 = 1; + + @Override + public void writeObject(ObjectOutput output, AuthenticatedClientSessionStore obj) throws IOException { + output.writeByte(VERSION_1); + + KeycloakMarshallUtil.writeMap(obj.authenticatedClientSessionIds, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.UUID_EXT, output); + } + + @Override + public AuthenticatedClientSessionStore readObject(ObjectInput input) throws IOException, ClassNotFoundException { + switch (input.readByte()) { + case VERSION_1: + return readObjectVersion1(input); + default: + throw new IOException("Unknown version"); + } + } + + public AuthenticatedClientSessionStore readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException { + AuthenticatedClientSessionStore res = new AuthenticatedClientSessionStore( + KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.UUID_EXT, ConcurrentHashMap::new) + ); + return res; + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java index 78df451b60..dbde092725 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java @@ -78,7 +78,7 @@ public class UserSessionEntity extends SessionEntity { private Map notes = new ConcurrentHashMap<>(); - private Map authenticatedClientSessions = new ConcurrentHashMap<>(); + private AuthenticatedClientSessionStore authenticatedClientSessions = new AuthenticatedClientSessionStore(); public String getUser() { return user; @@ -144,11 +144,11 @@ public class UserSessionEntity extends SessionEntity { this.notes = notes; } - public Map getAuthenticatedClientSessions() { + public AuthenticatedClientSessionStore getAuthenticatedClientSessions() { return authenticatedClientSessions; } - public void setAuthenticatedClientSessions(Map authenticatedClientSessions) { + public void setAuthenticatedClientSessions(AuthenticatedClientSessionStore authenticatedClientSessions) { this.authenticatedClientSessions = authenticatedClientSessions; } @@ -264,8 +264,7 @@ public class UserSessionEntity extends SessionEntity { Map notes = session.getNotes(); KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output); - Map authSessions = session.getAuthenticatedClientSessions(); - KeycloakMarshallUtil.writeMap(authSessions, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), output); + output.writeObject(session.getAuthenticatedClientSessions()); } @@ -285,7 +284,8 @@ public class UserSessionEntity extends SessionEntity { sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input)); sessionEntity.setBrokerSessionId(MarshallUtil.unmarshallString(input)); sessionEntity.setBrokerUserId(MarshallUtil.unmarshallString(input)); - sessionEntity.setId(MarshallUtil.unmarshallString(input)); + final String userSessionId = MarshallUtil.unmarshallString(input); + sessionEntity.setId(userSessionId); sessionEntity.setIpAddress(MarshallUtil.unmarshallString(input)); sessionEntity.setLoginUsername(MarshallUtil.unmarshallString(input)); sessionEntity.setRealmId(MarshallUtil.unmarshallString(input)); @@ -301,8 +301,7 @@ public class UserSessionEntity extends SessionEntity { new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>()); sessionEntity.setNotes(notes); - Map authSessions = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), - new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>()); + AuthenticatedClientSessionStore authSessions = (AuthenticatedClientSessionStore) input.readObject(); sessionEntity.setAuthenticatedClientSessions(authSessions); return sessionEntity; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java index b96b9bd7c8..128a7e98a4 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java @@ -142,7 +142,7 @@ public class RemoteCacheSessionsLoader implements SessionLoader { .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE) .get(OfflinePersistentUserSessionLoader.PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC); - if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) { + if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) { log.debugf("Sessions already loaded in current DC. Skip sessions loading from remote cache '%s'", cacheName); return true; } else { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java index d240b13bc5..0b9a764db3 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java @@ -17,8 +17,8 @@ package org.keycloak.models.sessions.infinispan.stream; +import org.keycloak.models.sessions.infinispan.AuthenticatedClientSessionAdapter; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; @@ -54,6 +54,11 @@ public class UserSessionPredicate implements Predicate> entry) { - SessionEntity e = entry.getValue().getEntity(); - - UserSessionEntity entity = (UserSessionEntity) e; + UserSessionEntity entity = entry.getValue().getEntity(); if (!realm.equals(entity.getRealmId())) { return false; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java index df5c8c673a..a1d83669dc 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.infinispan.commons.marshall.Externalizer; @@ -41,7 +42,19 @@ public class KeycloakMarshallUtil { private static final Logger log = Logger.getLogger(KeycloakMarshallUtil.class); - public static final StringExternalizer STRING_EXT = new StringExternalizer(); + public static final Externalizer STRING_EXT = new StringExternalizer(); + + public static final Externalizer UUID_EXT = new Externalizer() { + @Override + public void writeObject(ObjectOutput output, UUID uuid) throws IOException { + MarshallUtil.marshallUUID(uuid, output, true); + } + + @Override + public UUID readObject(ObjectInput input) throws IOException, ClassNotFoundException { + return MarshallUtil.unmarshallUUID(input, true); + } + }; // MAP diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java index ca06914f58..fb0394a133 100644 --- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java @@ -77,8 +77,8 @@ public class ConcurrencyJDGRemoteCacheTest { } private static Worker createWorker(int threadId) { - EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); - Cache cache = manager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); + Cache cache = manager.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); System.out.println("Retrieved cache: " + threadId); diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java index 5b7abe0695..536ee33225 100644 --- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java @@ -41,6 +41,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import java.util.UUID; /** * Check that removing of session from remoteCache is session immediately removed on remoteCache in other DC. This is true. @@ -68,9 +69,11 @@ public class ConcurrencyJDGRemoveSessionTest { //private static Map state = new HashMap<>(); + private static final UUID CLIENT_1_UUID = UUID.randomUUID(); + public static void main(String[] args) throws Exception { - Cache> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); - Cache> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> cache1 = createManager(1).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); + Cache> cache2 = createManager(2).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); // Create caches, listeners and finally worker threads Thread worker1 = createWorker(cache1, 1); @@ -177,7 +180,7 @@ public class ConcurrencyJDGRemoveSessionTest { clientSession.setTimestamp(1234); clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); - session.getAuthenticatedClientSessions().put("client1", clientSession); + session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId()); SessionEntityWrapper wrappedSession = new SessionEntityWrapper<>(session); return wrappedSession; @@ -205,7 +208,7 @@ public class ConcurrencyJDGRemoveSessionTest { private static EmbeddedCacheManager createManager(int threadId) { - return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); + return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); } diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java index 2b175baca0..2dfb5085d3 100644 --- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java @@ -36,7 +36,6 @@ import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; import org.infinispan.context.Flag; import org.infinispan.manager.EmbeddedCacheManager; import org.jboss.logging.Logger; -import org.junit.Assert; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; @@ -44,6 +43,7 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessi import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import java.util.UUID; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; /** @@ -74,9 +74,11 @@ public class ConcurrencyJDGSessionsCacheTest { //private static Map state = new HashMap<>(); + private static final UUID CLIENT_1_UUID = UUID.randomUUID(); + public static void main(String[] args) throws Exception { - Cache> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); - Cache> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> cache1 = createManager(1).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); + Cache> cache2 = createManager(2).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); // Create initial item UserSessionEntity session = new UserSessionEntity(); @@ -96,7 +98,7 @@ public class ConcurrencyJDGSessionsCacheTest { clientSession.setTimestamp(1234); clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); - session.getAuthenticatedClientSessions().put("client1", clientSession); + session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId()); SessionEntityWrapper wrappedSession = new SessionEntityWrapper<>(session); @@ -219,7 +221,7 @@ public class ConcurrencyJDGSessionsCacheTest { private static EmbeddedCacheManager createManager(int threadId) { - return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); + return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); } diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java index a5aae29251..a9bb784b2f 100644 --- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java @@ -38,6 +38,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import java.util.UUID; /** * Test concurrent writes to distributed cache with usage of atomic replace @@ -52,6 +53,8 @@ public class DistributedCacheConcurrentWritesTest { private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0); private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0); + private static final UUID CLIENT_1_UUID = UUID.randomUUID(); + public static void main(String[] args) throws Exception { CacheWrapper cache1 = createCache("node1"); CacheWrapper cache2 = createCache("node2"); @@ -74,7 +77,7 @@ public class DistributedCacheConcurrentWritesTest { clientSession.setTimestamp(1234); clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); - session.getAuthenticatedClientSessions().put("client1", clientSession); + session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId()); cache1.put("123", session); @@ -211,7 +214,7 @@ public class DistributedCacheConcurrentWritesTest { public static CacheWrapper createCache(String nodeName) { EmbeddedCacheManager mgr = createManager(nodeName); - Cache> wrapped = mgr.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> wrapped = mgr.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); return new CacheWrapper<>(wrapped); } @@ -245,7 +248,7 @@ public class DistributedCacheConcurrentWritesTest { } Configuration distConfig = distConfigBuilder.build(); - cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig); + cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, distConfig); return cacheManager; } diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java index 2ea5245583..d6bc350377 100644 --- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java @@ -35,27 +35,29 @@ import org.infinispan.transaction.LockingMode; import org.infinispan.transaction.lookup.DummyTransactionManagerLookup; import org.infinispan.util.concurrent.IsolationLevel; import org.jgroups.JChannel; -import org.junit.Ignore; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import java.util.UUID; /** * Test concurrent writes to distributed cache with usage of write skew * * @author Marek Posolda */ -@Ignore +//@Ignore public class DistributedCacheWriteSkewTest { private static final int ITERATION_PER_WORKER = 1000; private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0); + private static final UUID CLIENT_1_UUID = UUID.randomUUID(); + public static void main(String[] args) throws Exception { - Cache cache1 = createManager("node1").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); - Cache cache2 = createManager("node2").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache cache1 = createManager("node1").getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); + Cache cache2 = createManager("node2").getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); // Create initial item UserSessionEntity session = new UserSessionEntity(); @@ -75,7 +77,7 @@ public class DistributedCacheWriteSkewTest { clientSession.setTimestamp(1234); clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); - session.getAuthenticatedClientSessions().put("client1", clientSession); + session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId()); cache1.put("123", session); @@ -149,6 +151,7 @@ public class DistributedCacheWriteSkewTest { replaced = true; } catch (Exception e) { System.out.println(e); + e.printStackTrace(); failedReplaceCounter.incrementAndGet(); } @@ -208,7 +211,7 @@ public class DistributedCacheWriteSkewTest { } Configuration distConfig = distConfigBuilder.build(); - cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig); + cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, distConfig); return cacheManager; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java index 670e9cb3cd..f38b31cec9 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java @@ -116,6 +116,10 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate } @Override + public void detachFromUserSession() { + setUserSession(null); + } + public void setUserSession(UserSessionModel userSession) { this.userSession = userSession; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index e3b577766d..095a85782c 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -26,6 +26,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.util.JsonSerialization; import java.io.IOException; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -161,7 +162,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel { } @Override - public void removeAuthenticatedClientSessions(Iterable removedClientUUIDS) { + public void removeAuthenticatedClientSessions(Collection removedClientUUIDS) { if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) { return; } diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java index cee10e18b5..c54533c606 100644 --- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java @@ -27,7 +27,10 @@ import org.keycloak.sessions.CommonClientSessionModel; */ public interface AuthenticatedClientSessionModel extends CommonClientSessionModel { - void setUserSession(UserSessionModel userSession); + /** + * Detaches the client session from its user session. + */ + void detachFromUserSession(); UserSessionModel getUserSession(); String getCurrentRefreshToken(); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java index ff7c86485c..40fdada880 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java @@ -17,6 +17,7 @@ package org.keycloak.models; +import java.util.Collection; import java.util.Map; /** @@ -53,15 +54,22 @@ public interface UserSessionModel { void setLastSessionRefresh(int seconds); /** - * Returns map where key is ID of the client (its UUID) and value is the respective {@link AuthenticatedClientSessionModel} object. + * Returns map where key is ID of the client (its UUID) and value is ID respective {@link AuthenticatedClientSessionModel} object. * @return */ Map getAuthenticatedClientSessions(); + /** + * Returns a client session for the given client UUID. + * @return + */ + default AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) { + return getAuthenticatedClientSessions().get(clientUUID); + }; /** * Removes authenticated client sessions for all clients whose UUID is present in {@code removedClientUUIDS} parameter. * @param removedClientUUIDS */ - void removeAuthenticatedClientSessions(Iterable removedClientUUIDS); + void removeAuthenticatedClientSessions(Collection removedClientUUIDS); public String getNote(String name); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index 8334838b8c..cd265f2617 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -20,6 +20,7 @@ package org.keycloak.models; import org.keycloak.provider.Provider; import java.util.List; +import java.util.UUID; import java.util.function.Predicate; /** @@ -29,6 +30,7 @@ import java.util.function.Predicate; public interface UserSessionProvider extends Provider { AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession); + AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline); UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId); UserSessionModel getUserSession(RealmModel realm, String id); @@ -39,7 +41,7 @@ public interface UserSessionProvider extends Provider { UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId); /** - * Return userSession of specified ID as long as the predicate passes. Otherwise returs null. + * Return userSession of specified ID as long as the predicate passes. Otherwise returns {@code null}. * If predicate doesn't pass, implementation can do some best-effort actions to try have predicate passing (eg. download userSession from other DC) */ UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate predicate); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 4a0861466c..d0774c48e7 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -159,13 +159,13 @@ public class TokenManager { } ClientModel client = session.getContext().getClient(); - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); // Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache if (clientSession == null) { userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSession.getId(), offline, client.getId()); if (userSession != null) { - clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); } else { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session doesn't have required client", "Session doesn't have required client"); } @@ -400,7 +400,7 @@ public class TokenManager { public static AuthenticatedClientSessionModel attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) { ClientModel client = authSession.getClient(); - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); if (clientSession == null) { clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession); } @@ -436,8 +436,9 @@ public class TokenManager { return; } - clientSession.setUserSession(null); + clientSession.detachFromUserSession(); + // TODO: Might need optimization to prevent loading client sessions from cache in getAuthenticatedClientSessions() if (userSession.getAuthenticatedClientSessions().isEmpty()) { sessions.removeUserSession(realm, userSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index cf85aa4e55..562a2082e4 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -254,7 +254,7 @@ public class TokenEndpoint { // Attempt to use same code twice should invalidate existing clientSession if (clientSession != null) { - clientSession.setUserSession(null); + clientSession.detachFromUserSession(); } event.error(Errors.INVALID_CODE); @@ -400,7 +400,7 @@ public class TokenEndpoint { if (!result.isOfflineToken()) { UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState()); - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); updateClientSession(clientSession); updateUserSessionFromClientAuth(userSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index 9571fdeeab..0c913d0319 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -166,7 +166,7 @@ public class UserInfoEndpoint { // Existence of authenticatedClientSession for our client already handled before - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId()); AccessToken userInfo = new AccessToken(); tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession); diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java index afea781ded..ec2fa26dcb 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -400,7 +400,7 @@ public class SamlService extends AuthorizationEndpointBase { userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod()); userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL); // remove client from logout requests - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); if (clientSession != null) { clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name()); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java index 83b54dde73..f9354b4a1c 100644 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java @@ -62,7 +62,7 @@ public class SamlSessionUtils { return null; } - return userSession.getAuthenticatedClientSessions().get(parts[1]); + return userSession.getAuthenticatedClientSessionByClient(clientUUID); } } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 7c779557a4..1e8a53d5fb 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -395,7 +395,7 @@ public class AuthenticationManager { public static void backchannelLogoutUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) { List userSessions = session.sessions().getUserSessions(realm, user); for (UserSessionModel userSession : userSessions) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); if (clientSession != null) { AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers); clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name()); diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java index 70e3f353cf..de141ff0b0 100644 --- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java +++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java @@ -172,7 +172,7 @@ class CodeGenerateUtil { } } - return userSession.getAuthenticatedClientSessions().get(codeJWT.getIssuedFor()); + return userSession.getAuthenticatedClientSessionByClient(codeJWT.getIssuedFor()); } diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java index de2516fefc..476022c0cd 100644 --- a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java @@ -42,7 +42,7 @@ public class UserSessionCrossDCManager { // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, boolean offline, String clientUUID) { - return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessions().containsKey(clientUUID)); + return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessionByClient(clientUUID) != null); } @@ -52,8 +52,8 @@ public class UserSessionCrossDCManager { return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> { - Map authSessions = userSession.getAuthenticatedClientSessions(); - return authSessions.containsKey(clientUUID); + AuthenticatedClientSessionModel authSessions = userSession.getAuthenticatedClientSessionByClient(clientUUID); + return authSessions != null; }); } diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java index f347d4ce69..1bef11e0fb 100644 --- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java @@ -63,7 +63,7 @@ public class UserSessionManager { } // Create and persist clientSession - AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessions().get(clientSession.getClient().getId()); + AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessionByClient(clientSession.getClient().getId()); if (offlineClientSession == null) { createOfflineClientSession(user, clientSession, offlineUserSession); } @@ -97,14 +97,14 @@ public class UserSessionManager { List userSessions = kcSession.sessions().getOfflineUserSessions(realm, user); boolean anyRemoved = false; for (UserSessionModel userSession : userSessions) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); if (clientSession != null) { if (logger.isTraceEnabled()) { logger.tracef("Removing existing offline token for user '%s' and client '%s' .", user.getUsername(), client.getClientId()); } - clientSession.setUserSession(null); + clientSession.detachFromUserSession(); persister.removeClientSession(userSession.getId(), client.getId(), true); checkOfflineUserSessionHasClientSessions(realm, user, userSession); anyRemoved = true; @@ -154,7 +154,8 @@ public class UserSessionManager { // Check if userSession has any offline clientSessions attached to it. Remove userSession if not private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, UserSessionModel userSession) { - if (userSession.getAuthenticatedClientSessions().size() > 0) { + // TODO: Might need optimization to prevent loading client sessions from cache + if (! userSession.getAuthenticatedClientSessions().isEmpty()) { return; } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index 1f73bc643f..3cfb6d3833 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -142,7 +142,7 @@ public class AccountFormService extends AbstractSecuredLocalService { if (authResult != null) { UserSessionModel userSession = authResult.getSession(); if (userSession != null) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); if (clientSession == null) { clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 34bbcea9ce..dcfd65115b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -339,7 +339,7 @@ public class UserResource { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); // Update lastSessionRefresh with the timestamp from clientSession - AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessions().get(clientId); + AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessionByClient(clientId); // Skip if userSession is not for this client if (clientSession == null) { diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli b/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli index 3eee2fcc2e..fd086665cf 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli @@ -60,6 +60,38 @@ echo ** Update distributed-cache offlineSessions element ** ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true) +echo ** Update distributed-cache clientSessions element ** +/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( \ + passivation=false, \ + fetch-state=false, \ + purge=false, \ + preload=false, \ + shared=true, \ + remote-servers=["remote-cache"], \ + cache=clientSessions, \ + properties={ \ + rawValues=true, \ + marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \ + } \ +) +/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true) + +echo ** Update distributed-cache offlineClientSessions element ** +/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( \ + passivation=false, \ + fetch-state=false, \ + purge=false, \ + preload=false, \ + shared=true, \ + remote-servers=["remote-cache"], \ + cache=offlineClientSessions, \ + properties={ \ + rawValues=true, \ + marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \ + } \ +) +/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=statistics-enabled,value=true) + echo ** Update distributed-cache loginFailures element ** /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( \ passivation=false, \ diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index d5395f2bbf..697731021e 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -185,6 +185,7 @@ public class TestingResourceProvider implements RealmResourceProvider { throw new NotFoundException("Session not found"); } + // TODO: Might need optimization to prevent loading client sessions from cache return sessionModel.getAuthenticatedClientSessions().size(); } diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl index efc0400a0e..8bd5149ea5 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl @@ -42,9 +42,10 @@ - + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java index 0986c1791d..9b688782d4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java @@ -76,7 +76,7 @@ public class ConcurrentLoginClusterTest extends ConcurrentLoginTest { @Override public void concurrentLoginSingleUser() throws Throwable { super.concurrentLoginSingleUser(); - JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getJgroupsStats(); + JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getJgroupsStats(); log.info("JGroups statistics: " + stats.statsAsString()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java index 76f6a7e4cc..2c1ca21971 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java @@ -388,10 +388,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest private void setTimeOffsetOnAllStartedContainers(int offset) { backendTestingClients.entrySet().stream() .filter(testingClientEntry -> testingClientEntry.getKey().isStarted()) - .forEach(testingClientEntry -> { - KeycloakTestingClient testingClient = testingClientEntry.getValue(); - testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset))); - }); + .map(testingClientEntry -> testingClientEntry.getValue()) + .forEach(testingClient -> testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset)))); } /** diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java index bf42536372..8f04936f69 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java @@ -26,7 +26,6 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Retry; import org.keycloak.testsuite.arquillian.ContainerInfo; -import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.rest.representation.RemoteCacheStats; import org.keycloak.testsuite.util.OAuthClient; @@ -57,7 +56,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { // Get statistics int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId); int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId); - int lsrr0 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); + int lsrr0 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); log.infof("lsr00: %d, lsr10: %d, lsrr0: %d", lsr00, lsr10, lsrr0); Assert.assertEquals(lsr00, lsr10); @@ -76,7 +75,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { Retry.execute(() -> { int lsr01 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId); int lsr11 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId); - int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); + int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); log.infof("lsr01: %d, lsr11: %d, lsrr1: %d", lsr01, lsr11, lsrr1); Assert.assertEquals(lsr01, lsr11); @@ -88,7 +87,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { disableDcOnLoadBalancer(DC.FIRST); enableDcOnLoadBalancer(DC.SECOND); tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); - Assert.assertNull(tokenResponse.getAccessToken()); + Assert.assertNull("Expecting no access token present", tokenResponse.getAccessToken()); Assert.assertNotNull(tokenResponse.getError()); // try refresh with new token on DC1. It should pass. @@ -164,7 +163,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { Assert.assertEquals(lsr10, lsr11.get()); // assert that lastSessionRefresh still the same on remoteCache - int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); + int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); Assert.assertEquals(lsr00, lsrr1); log.infof("lsrr1: %d", lsrr1); @@ -187,7 +186,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { Assert.assertTrue(lsr02.get() > lsr01.get()); Assert.assertTrue(lsr12.get() > lsr11.get()); - lsrr2.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId)); + lsrr2.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId)); log.infof("lsrr2: %d", lsrr2.get()); Assert.assertEquals(lsrr1, lsrr2.get()); @@ -218,7 +217,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { Assert.assertTrue(lsr03.get() > lsr02.get()); Assert.assertTrue(lsr13.get() > lsr12.get()); - lsrr3.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId)); + lsrr3.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId)); log.infof("lsrr3: %d", lsrr3.get()); Assert.assertTrue(lsrr3.get() > lsrr2.get()); @@ -232,7 +231,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { private RemoteCacheStats getRemoteCacheStats(int dcIndex) { return getTestingClientForStartedNodeInDc(dcIndex).testing("test") - .cache(InfinispanConnectionProvider.SESSION_CACHE_NAME) + .cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) .getRemoteCacheStats(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java index 2644a9af12..1523b33b64 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java @@ -102,10 +102,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { @Test public void testRealmRemoveSessions( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); + createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); // log.infof("Sleeping!"); // Thread.sleep(10000000); @@ -116,7 +118,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { getAdminClient().realm(REALM_NAME).remove(); // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 100l); } @@ -194,11 +196,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { @Test public void testRealmRemoveOfflineSessions( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true); + createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true); channelStatisticsCrossDc.reset(); @@ -206,18 +208,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { getAdminClient().realm(REALM_NAME).remove(); // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 200l); // Might be bigger messages as online sessions removed too. } @Test public void testLogoutAllInRealm( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); + createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); channelStatisticsCrossDc.reset(); @@ -225,18 +227,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { getAdminClient().realm(REALM_NAME).logoutAll(); // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 100l); } @Test public void testPeriodicExpiration( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1); + OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1); // Assert I am able to refresh OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password"); @@ -249,7 +251,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); // Nothing yet expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened - assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, 5l); @@ -268,7 +270,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 100l); } @@ -277,10 +279,10 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { @Test public void testUserRemoveSessions( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); + createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); // log.infof("Sleeping!"); // Thread.sleep(10000000); @@ -292,17 +294,17 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After user remove", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After user remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 100l); } @Test public void testUserRemoveOfflineSessions( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true); + createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true); // log.infof("Sleeping!"); // Thread.sleep(10000000); @@ -314,18 +316,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 100l); } @Test public void testLogoutUser( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); + createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true); channelStatisticsCrossDc.reset(); @@ -335,29 +337,29 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId()); // Just one session expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened - assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01 + SESSIONS_COUNT - 1, sessions02 + SESSIONS_COUNT - 1, remoteSessions01 + SESSIONS_COUNT - 1, remoteSessions02 + SESSIONS_COUNT - 1, 5l); // Logout all sessions for user now user.logout(); // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. - assertStatisticsExpected("After user logout", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + assertStatisticsExpected("After user logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, sessions01, sessions02, remoteSessions01, remoteSessions02, 100l); } @Test public void testLogoutUserWithFailover( - @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, - @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { // Start node2 on first DC startBackendNode(DC.FIRST, 1); // Don't include remote stats. Size is smaller because of distributed cache - List responses = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false); + List responses = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false); // Kill node2 now. Around 10 sessions (half of SESSIONS_COUNT) will be lost on Keycloak side. But not on infinispan side stopBackendNode(DC.FIRST, 1); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java index f4a715944d..bc1243b47c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java @@ -112,7 +112,7 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest { @Test public void sessionsPreloadTest() throws Exception { - int sessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).size(); + int sessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(); log.infof("sessionsBefore: %d", sessionsBefore); // Create initial sessions @@ -124,8 +124,8 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest { enableLoadBalancerNode(DC.SECOND, 0); // Ensure sessions are loaded in both 1st DC and 2nd DC - int sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).size(); - int sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).size(); + int sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(); + int sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(); log.infof("sessions01: %d, sessions02: %d", sessions01, sessions02); Assert.assertEquals(sessions01, sessionsBefore + SESSIONS_COUNT); Assert.assertEquals(sessions02, sessionsBefore + SESSIONS_COUNT); @@ -144,13 +144,13 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest { @Test public void offlineSessionsPreloadTest() throws Exception { - int offlineSessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size(); + int offlineSessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size(); log.infof("offlineSessionsBefore: %d", offlineSessionsBefore); // Create initial sessions List tokenResponses = createInitialSessions(true); - int offlineSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size(); + int offlineSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size(); Assert.assertEquals(offlineSessions01, offlineSessionsBefore + SESSIONS_COUNT); log.infof("offlineSessions01: %d", offlineSessions01); @@ -168,8 +168,8 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest { enableLoadBalancerNode(DC.SECOND, 0); // Ensure sessions are loaded in both 1st DC and 2nd DC - int offlineSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size(); - int offlineSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size(); + int offlineSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size(); + int offlineSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size(); log.infof("offlineSessions11: %d, offlineSessions12: %d", offlineSessions11, offlineSessions12); Assert.assertEquals(offlineSessions11, offlineSessionsBefore + SESSIONS_COUNT); Assert.assertEquals(offlineSessions12, offlineSessionsBefore + SESSIONS_COUNT); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 5bbf71ed58..78ec0e7aa0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -562,7 +562,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest RealmModel realmModel = session.getContext().getRealm(); String clientUuid = realmModel.getClientByClientId(clientId).getId(); UserSessionModel userSession = session.sessions().getUserSession(realmModel, sessionId); - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUuid); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUuid); String claimsInSession = clientSession.getNote(OIDCLoginProtocol.CLAIMS_PARAM); assertEquals(claimsJson, claimsInSession); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java index 894612725a..569a076e34 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java @@ -169,7 +169,7 @@ public class LastSessionRefreshUnitTest extends AbstractKeycloakTest { }; - Cache> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); return factory.createAndInit(session, cache, timerIntervalMs, maxIntervalBetweenMessagesSeconds, 10, false); } diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java index 8c01e2ceaf..9162726b1a 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -132,7 +132,6 @@ public class UserSessionInitializerTest { private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); - if (userSession != null) clientSession.setUserSession(userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); if (roles != null) clientSession.setRoles(roles); diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java index 8c89046eb2..efa2f7d7ed 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -362,7 +362,6 @@ public class UserSessionPersisterProviderTest { private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); - if (userSession != null) clientSession.setUserSession(userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); if (roles != null) clientSession.setRoles(roles); diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index 106f5258c7..265ecd79da 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -396,7 +396,6 @@ public class UserSessionProviderOfflineTest { private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client, userSession); - if (userSession != null) clientSession.setUserSession(userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); if (roles != null) clientSession.setRoles(roles); diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index e6163315fb..3e2a4dc2fd 100755 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -33,7 +33,6 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.models.UserManager; -import org.keycloak.sessions.CommonClientSessionModel; import org.keycloak.testsuite.rule.KeycloakRule; import java.util.Arrays; @@ -181,6 +180,30 @@ public class UserSessionProviderTest { assertEquals(time + 10, updated.getTimestamp()); } + @Test + public void testUpdateClientSessionWithGetByClientId() { + UserSessionModel[] sessions = createSessions(); + + String userSessionId = sessions[0].getId(); + String clientUUID = realm.getClientByClientId("test-app").getId(); + + UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID); + + int time = clientSession.getTimestamp(); + assertEquals(null, clientSession.getAction()); + + clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); + clientSession.setTimestamp(time + 10); + + kc.stopSession(session, true); + session = kc.startSession(); + + AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessionByClient(clientUUID); + assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction()); + assertEquals(time + 10, updated.getTimestamp()); + } + @Test public void testUpdateClientSessionInSameTransaction() { UserSessionModel[] sessions = createSessions(); @@ -189,12 +212,12 @@ public class UserSessionProviderTest { String clientUUID = realm.getClientByClientId("test-app").getId(); UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID); clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); clientSession.setNote("foo", "bar"); - AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID); + AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessionByClient(clientUUID); assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction()); assertEquals("bar", updated.getNote("foo")); } @@ -361,7 +384,6 @@ public class UserSessionProviderTest { Time.setOffset(i); UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null); AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession); - clientSession.setUserSession(userSession); clientSession.setRedirectUri("http://redirect"); clientSession.setRoles(new HashSet()); clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state"); @@ -451,7 +473,7 @@ public class UserSessionProviderTest { // remove session clientSession1 = userSession.getAuthenticatedClientSessions().get(client1.getId()); - clientSession1.setUserSession(null); + clientSession1.detachFromUserSession(); // Commit and ensure removed resetSession(); diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java index 41495d4060..4790e9cbe7 100644 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java @@ -31,19 +31,28 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.utils.KeycloakModelUtils; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; /** * @author Marek Posolda */ public abstract class AbstractSessionCacheCommand extends AbstractCommand { + private static final Set SUPPORTED_CACHE_NAMES = new TreeSet<>(Arrays.asList( + InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, + InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, + InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, + InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME + )); + @Override protected void doRunCommand(KeycloakSession session) { InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); String cacheName = getArg(0); - if (!cacheName.equals(InfinispanConnectionProvider.SESSION_CACHE_NAME) && !cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME)) { - log.errorf("Invalid cache name: '%s', Only cache names '%s' or '%s' are supported", cacheName, InfinispanConnectionProvider.SESSION_CACHE_NAME, - InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + if (! SUPPORTED_CACHE_NAMES.contains(cacheName)) { + log.errorf("Invalid cache name: '%s', Only cache names '%s' are supported", cacheName, SUPPORTED_CACHE_NAMES); throw new HandledException(); } diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java index 53e97a5e58..563df2083d 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java @@ -41,7 +41,7 @@ import java.util.List; public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcessor { private static final String[] CACHES = new String[] { - "realms", "users","sessions","authenticationSessions","offlineSessions","loginFailures","work","authorization","keys","actionTokens" + "realms", "users","sessions","authenticationSessions","offlineSessions","clientSessions","offlineClientSessions","loginFailures","work","authorization","keys","actionTokens" }; // This param name is defined again in Keycloak Services class diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index 8702948345..b3ea2a9d5f 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -34,6 +34,8 @@ + + @@ -94,6 +96,8 @@ + +