From f92fe6bea92a2b080ec4e71f3835d1ca22e53637 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 5 Oct 2015 16:52:45 +0200 Subject: [PATCH] KEYCLOAK-904 Offline tokens storage changes. Added UserSessionPersisterProvider . offline sessions preloaded to cache at startup --- connections/infinispan/pom.xml | 1 + ...ltInfinispanConnectionProviderFactory.java | 13 + .../InfinispanConnectionProvider.java | 1 + .../META-INF/jpa-changelog-1.6.0.xml | 23 +- .../main/resources/META-INF/persistence.xml | 4 +- ...DefaultMongoConnectionFactoryProvider.java | 6 +- .../configuration/keycloak-server.json | 4 + .../exportimport/util/ExportUtils.java | 47 +- .../freemarker/model/ApplicationsBean.java | 4 +- .../java/org/keycloak/models/RealmModel.java | 7 + .../models/UserFederationManager.java | 64 --- .../org/keycloak/models/UserProvider.java | 12 - .../org/keycloak/models/UserSessionModel.java | 1 + .../keycloak/models/UserSessionProvider.java | 19 + .../models/UserSessionProviderFactory.java | 4 + .../entities/OfflineUserSessionEntity.java | 37 -- ...ava => PersistentClientSessionEntity.java} | 2 +- .../entities/PersistentUserSessionEntity.java | 64 +++ .../keycloak/models/entities/UserEntity.java | 9 - .../DisabledUserSessionPersisterProvider.java | 104 +++++ .../PersistentClientSessionAdapter.java | 401 ++++++++++++++++++ .../PersistentClientSessionModel.java} | 4 +- .../session/PersistentUserSessionAdapter.java | 109 ++++- .../PersistentUserSessionModel.java} | 15 +- .../session/UserSessionPersisterProvider.java | 43 ++ .../UserSessionPersisterProviderFactory.java | 9 + .../session/UserSessionPersisterSpi.java | 31 ++ .../models/utils/ModelToRepresentation.java | 10 +- .../models/utils/RepresentationToModel.java | 54 ++- .../models/utils/UserModelDelegate.java | 3 - ...ession.UserSessionPersisterProviderFactory | 1 + .../services/org.keycloak.provider.Spi | 1 + .../models/file/FileUserProvider.java | 192 +-------- .../models/file/adapter/UserAdapter.java | 8 - .../infinispan/DefaultCacheUserProvider.java | 98 +---- .../models/cache/entities/CachedUser.java | 27 +- .../models/jpa/JpaKeycloakTransaction.java | 53 --- .../keycloak/models/jpa/JpaUserProvider.java | 185 -------- .../org/keycloak/models/jpa/UserAdapter.java | 7 - .../entities/OfflineClientSessionEntity.java | 83 ---- .../entities/OfflineUserSessionEntity.java | 59 --- .../models/jpa/entities/UserEntity.java | 28 -- .../JpaUserSessionPersisterProvider.java | 239 +++++++++++ ...paUserSessionPersisterProviderFactory.java | 44 ++ .../PersistentClientSessionEntity.java | 132 ++++++ .../session/PersistentUserSessionEntity.java | 147 +++++++ ...ession.UserSessionPersisterProviderFactory | 1 + .../keycloak/adapters/MongoUserProvider.java | 239 ----------- .../MongoUserSessionPersisterProvider.java | 276 ++++++++++++ ...goUserSessionPersisterProviderFactory.java | 42 ++ .../mongo/keycloak/adapters/UserAdapter.java | 6 - .../MongoOfflineUserSessionEntity.java | 10 + .../MongoOnlineUserSessionEntity.java | 10 + .../entities/MongoUserSessionEntity.java | 15 + ...ession.UserSessionPersisterProviderFactory | 1 + model/sessions-infinispan/pom.xml | 5 + .../infinispan/ClientSessionAdapter.java | 20 +- .../InfinispanUserSessionProvider.java | 317 +++++++++++--- .../InfinispanUserSessionProviderFactory.java | 88 +++- .../infinispan/UserSessionAdapter.java | 17 +- .../compat/MemUserSessionProvider.java | 219 ++++++++-- .../compat/MemUserSessionProviderFactory.java | 6 +- .../compat/SimpleUserSessionInitializer.java | 37 ++ .../infinispan/compat/UserSessionAdapter.java | 5 + .../entities/ClientSessionEntity.java | 10 - .../InfinispanUserSessionInitializer.java | 268 ++++++++++++ .../initializer/InitializerState.java | 108 +++++ .../initializer/OfflineUserSessionLoader.java | 49 +++ .../initializer/SessionInitializerWorker.java | 65 +++ .../infinispan/initializer/SessionLoader.java | 15 + .../ClientSessionsOfUserSessionMapper.java | 44 ++ .../initializer/InitializerStateTest.java | 45 ++ .../keycloak/protocol/oidc/TokenManager.java | 9 +- .../services/managers/ClientManager.java | 6 + .../services/managers/RealmManager.java | 6 + .../services/managers/UserManager.java | 7 + .../services/managers/UserSessionManager.java | 139 ++++++ .../offline/OfflineClientSessionAdapter.java | 300 ------------- .../services/offline/OfflineTokenUtils.java | 192 --------- .../services/resources/AccountService.java | 4 +- .../resources/admin/ClientResource.java | 23 +- .../resources/admin/UsersResource.java | 6 +- .../resources/META-INF/keycloak-server.json | 4 + .../org/keycloak/testsuite/InfinispanCLI.java | 226 ++++++++++ .../keycloak/testsuite/KeycloakServer.java | 4 + .../keycloak/testsuite/model/ImportTest.java | 31 +- .../testsuite/model/UserModelTest.java | 158 +++---- .../model/UserSessionInitializerTest.java | 162 +++++++ .../UserSessionPersisterProviderTest.java | 335 +++++++++++++++ .../model/UserSessionProviderOfflineTest.java | 348 +++++++++++++++ .../model/UserSessionProviderTest.java | 18 +- .../testsuite/oauth/OfflineTokenTest.java | 19 +- .../resources/META-INF/keycloak-server.json | 18 + 93 files changed, 4397 insertions(+), 1945 deletions(-) delete mode 100644 model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java rename model/api/src/main/java/org/keycloak/models/entities/{OfflineClientSessionEntity.java => PersistentClientSessionEntity.java} (93%) create mode 100644 model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java create mode 100644 model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java create mode 100644 model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java rename model/api/src/main/java/org/keycloak/models/{OfflineClientSessionModel.java => session/PersistentClientSessionModel.java} (92%) rename services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java => model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java (57%) rename model/api/src/main/java/org/keycloak/models/{OfflineUserSessionModel.java => session/PersistentUserSessionModel.java} (58%) create mode 100644 model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java create mode 100644 model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java create mode 100644 model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java create mode 100644 model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory delete mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakTransaction.java delete mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java delete mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java create mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java create mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java create mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java create mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java create mode 100644 model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory create mode 100644 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java create mode 100644 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java create mode 100644 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java create mode 100644 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java create mode 100644 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java create mode 100644 model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java create mode 100644 model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java create mode 100644 services/src/main/java/org/keycloak/services/managers/UserSessionManager.java delete mode 100644 services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java delete mode 100644 services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java diff --git a/connections/infinispan/pom.xml b/connections/infinispan/pom.xml index 711de79ad6..3bfefdf91d 100755 --- a/connections/infinispan/pom.xml +++ b/connections/infinispan/pom.xml @@ -27,5 +27,6 @@ infinispan-core provided + diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 58962dc5c3..bc2635c418 100755 --- a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -66,6 +66,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon } else { initEmbedded(); } + + // Backwards compatibility + if (cacheManager.getCacheConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) == null) { + logger.warnf("No configuration provided for '%s' cache. Using '%s' configuration as template", + InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, InfinispanConnectionProvider.SESSION_CACHE_NAME); + + Configuration sessionCacheConfig = cacheManager.getCacheConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME); + if (sessionCacheConfig != null) { + ConfigurationBuilder confBuilder = new ConfigurationBuilder().read(sessionCacheConfig); + Configuration offlineSessionConfig = confBuilder.build(); + cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionConfig); + } + } } } } diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index 05cfdb9e1d..945e0de47c 100644 --- a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -11,6 +11,7 @@ public interface InfinispanConnectionProvider extends Provider { static final String REALM_CACHE_NAME = "realms"; static final String USER_CACHE_NAME = "users"; static final String SESSION_CACHE_NAME = "sessions"; + static final String OFFLINE_SESSION_CACHE_NAME = "offlineSessions"; static final String LOGIN_FAILURE_CACHE_NAME = "loginFailures"; Cache getCache(String name); diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml index 9d6e545b5a..a782f76f79 100644 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml @@ -13,19 +13,23 @@ + + + - + + + + + - - - @@ -35,14 +39,13 @@ + + + - - - - - - + + \ No newline at end of file diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index f8f33be9ad..b8592a365c 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -29,8 +29,8 @@ org.keycloak.models.jpa.entities.AuthenticationExecutionEntity org.keycloak.models.jpa.entities.AuthenticatorConfigEntity org.keycloak.models.jpa.entities.RequiredActionProviderEntity - org.keycloak.models.jpa.entities.OfflineUserSessionEntity - org.keycloak.models.jpa.entities.OfflineClientSessionEntity + org.keycloak.models.jpa.session.PersistentUserSessionEntity + org.keycloak.models.jpa.session.PersistentClientSessionEntity org.keycloak.events.jpa.EventEntity diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index bd5a2a295e..82f3d57b5a 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -36,6 +36,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoOnlineUserSessionEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoOfflineUserSessionEntity", "org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", @@ -49,8 +51,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro "org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.RequiredActionProviderEntity", - "org.keycloak.models.entities.OfflineUserSessionEntity", - "org.keycloak.models.entities.OfflineClientSessionEntity", + "org.keycloak.models.entities.PersistentUserSessionEntity", + "org.keycloak.models.entities.PersistentClientSessionEntity", }; private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json index 188a2a2d3e..669cf41db5 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json @@ -22,6 +22,10 @@ "provider": "jpa" }, + "userSessionPersister": { + "provider": "jpa" + }, + "timer": { "provider": "basic" }, diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 88471d0b35..9963ea13d7 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -1,7 +1,7 @@ package org.keycloak.exportimport.util; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.representations.idm.OfflineUserSessionRepresentation; import org.keycloak.util.Base64; import org.codehaus.jackson.JsonEncoding; @@ -11,7 +11,6 @@ import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.SerializationConfig; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; @@ -298,27 +297,27 @@ public class ExportUtils { } } - // Offline sessions - List offlineSessionReps = new LinkedList<>(); - Collection offlineSessions = session.users().getOfflineUserSessions(realm, user); - Collection offlineClientSessions = session.users().getOfflineClientSessions(realm, user); - - Map> processed = new HashMap<>(); - for (OfflineClientSessionModel clsm : offlineClientSessions) { - String userSessionId = clsm.getUserSessionId(); - List current = processed.get(userSessionId); - if (current == null) { - current = new LinkedList<>(); - processed.put(userSessionId, current); - } - current.add(clsm); - } - - for (OfflineUserSessionModel userSession : offlineSessions) { - OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId())); - offlineSessionReps.add(sessionRep); - } - userRep.setOfflineUserSessions(offlineSessionReps); +// // Offline sessions +// List offlineSessionReps = new LinkedList<>(); +// Collection offlineSessions = session.users().getOfflineUserSessions(realm, user); +// Collection offlineClientSessions = session.users().getOfflineClientSessions(realm, user); +// +// Map> processed = new HashMap<>(); +// for (PersistentClientSessionModel clsm : offlineClientSessions) { +// String userSessionId = clsm.getUserSessionId(); +// List current = processed.get(userSessionId); +// if (current == null) { +// current = new LinkedList<>(); +// processed.put(userSessionId, current); +// } +// current.add(clsm); +// } +// +// for (PersistentUserSessionModel userSession : offlineSessions) { +// OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId())); +// offlineSessionReps.add(sessionRep); +// } +// userRep.setOfflineUserSessions(offlineSessionReps); return userRep; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java index 71bf2005d8..95ebe4fe44 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java @@ -13,7 +13,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.services.offline.OfflineTokenUtils; +import org.keycloak.services.managers.UserSessionManager; import org.keycloak.util.MultivaluedHashMap; /** @@ -25,7 +25,7 @@ public class ApplicationsBean { public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) { - Set offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user); + Set offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user); List realmClients = realm.getClients(); for (ClientModel client : realmClients) { diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 13d111d63c..452e2b35d9 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -97,6 +97,9 @@ public interface RealmModel extends RoleContainerModel { int getSsoSessionMaxLifespan(); void setSsoSessionMaxLifespan(int seconds); +// int getOfflineSessionIdleTimeout(); +// void setOfflineSessionIdleTimeout(int seconds); + int getAccessTokenLifespan(); void setAccessTokenLifespan(int seconds); @@ -286,6 +289,10 @@ public interface RealmModel extends RoleContainerModel { void setEventsEnabled(boolean enabled); +// boolean isPersistUserSessions(); +// +// void setPersistUserSessions(); + long getEventsExpiration(); void setEventsExpiration(long expiration); diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index b5020844f3..bea51e0473 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -475,70 +475,6 @@ public class UserFederationManager implements UserProvider { return (result != null) ? result : CredentialValidationOutput.failed(); } - @Override - public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - session.userStorage().addOfflineUserSession(realm, user, offlineUserSession); - } - - @Override - public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - return session.userStorage().getOfflineUserSession(realm, user, userSessionId); - } - - @Override - public Collection getOfflineUserSessions(RealmModel realm, UserModel user) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - return session.userStorage().getOfflineUserSessions(realm, user); - } - - @Override - public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - return session.userStorage().removeOfflineUserSession(realm, user, userSessionId); - } - - @Override - public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) { - session.userStorage().addOfflineClientSession(realm, offlineClientSession); - } - - @Override - public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - return session.userStorage().getOfflineClientSession(realm, user, clientSessionId); - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, UserModel user) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - return session.userStorage().getOfflineClientSessions(realm, user); - } - - @Override - public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) { - validateUser(realm, user); - if (user == null) throw new IllegalStateException("Federated user no longer valid"); - return session.userStorage().removeOfflineClientSession(realm, user, clientSessionId); - } - - @Override - public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) { - return session.userStorage().getOfflineClientSessionsCount(realm, client); - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - return session.userStorage().getOfflineClientSessions(realm, client, firstResult, maxResults); - } - @Override public void close() { } diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index 0012b24b0b..7d7064d76e 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -56,17 +56,5 @@ public interface UserProvider extends Provider { boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input); CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input); - void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession); - OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId); - Collection getOfflineUserSessions(RealmModel realm, UserModel user); - boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId); - void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession); - OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId); - Collection getOfflineClientSessions(RealmModel realm, UserModel user); - boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId); - - int getOfflineClientSessionsCount(RealmModel realm, ClientModel client); - Collection getOfflineClientSessions(RealmModel realm, ClientModel client, int first, int max); - void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java index 12ebd70c2f..8db5cdcffc 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java @@ -9,6 +9,7 @@ import java.util.Map; public interface UserSessionModel { String getId(); + RealmModel getRealm(); /** * If created via a broker external login, this is an identifier that can be diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index 3b4d1472d7..836cc75769 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -2,6 +2,7 @@ package org.keycloak.models; import org.keycloak.provider.Provider; +import java.util.Collection; import java.util.List; /** @@ -27,6 +28,8 @@ public interface UserSessionProvider extends Provider { int getActiveUserSessions(RealmModel realm, ClientModel client); void removeUserSession(RealmModel realm, UserSessionModel session); void removeUserSessions(RealmModel realm, UserModel user); + + // Implementation should propagate removal of expired userSessions to userSessionPersister too void removeExpiredUserSessions(RealmModel realm); void removeUserSessions(RealmModel realm); void removeClientSession(RealmModel realm, ClientSessionModel clientSession); @@ -40,6 +43,22 @@ public interface UserSessionProvider extends Provider { void onClientRemoved(RealmModel realm, ClientModel client); void onUserRemoved(RealmModel realm, UserModel user); + UserSessionModel createOfflineUserSession(UserSessionModel userSession); + UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId); + + // Removes the attached clientSessions as well + void removeOfflineUserSession(RealmModel realm, String userSessionId); + + ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession); + ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId); + List getOfflineClientSessions(RealmModel realm, UserModel user); + + // Don't remove userSession even if it's last userSession + void removeOfflineClientSession(RealmModel realm, String clientSessionId); + + int getOfflineSessionsCount(RealmModel realm, ClientModel client); + List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max); + void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java b/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java index b57b55e31d..93b6f322a4 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java @@ -7,4 +7,8 @@ import org.keycloak.provider.ProviderFactory; * @version $Revision: 1 $ */ public interface UserSessionProviderFactory extends ProviderFactory { + + // This is supposed to prefill all userSessions and clientSessions from userSessionPersister to the userSession infinispan/memory storage + void loadPersistentSessions(KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment); + } diff --git a/model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java deleted file mode 100644 index e7858983fb..0000000000 --- a/model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.keycloak.models.entities; - -import java.util.List; - -/** - * @author Marek Posolda - */ -public class OfflineUserSessionEntity { - - private String userSessionId; - private String data; - private List offlineClientSessions; - - public String getUserSessionId() { - return userSessionId; - } - - public void setUserSessionId(String userSessionId) { - this.userSessionId = userSessionId; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } - - public List getOfflineClientSessions() { - return offlineClientSessions; - } - - public void setOfflineClientSessions(List offlineClientSessions) { - this.offlineClientSessions = offlineClientSessions; - } -} diff --git a/model/api/src/main/java/org/keycloak/models/entities/OfflineClientSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java similarity index 93% rename from model/api/src/main/java/org/keycloak/models/entities/OfflineClientSessionEntity.java rename to model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java index 69ad60bb38..a03edd31d5 100644 --- a/model/api/src/main/java/org/keycloak/models/entities/OfflineClientSessionEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java @@ -3,7 +3,7 @@ package org.keycloak.models.entities; /** * @author Marek Posolda */ -public class OfflineClientSessionEntity { +public class PersistentClientSessionEntity { private String clientSessionId; private String clientId; diff --git a/model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java new file mode 100644 index 0000000000..dd5262cbf4 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java @@ -0,0 +1,64 @@ +package org.keycloak.models.entities; + +import java.util.List; + +/** + * @author Marek Posolda + */ +public class PersistentUserSessionEntity { + + private String id; + private String realmId; + private String userId; + private int lastSessionRefresh; + private String data; + private List clientSessions; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + public void setLastSessionRefresh(int lastSessionRefresh) { + this.lastSessionRefresh = lastSessionRefresh; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public List getClientSessions() { + return clientSessions; + } + + public void setClientSessions(List clientSessions) { + this.clientSessions = clientSessions; + } +} diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java index 66020db921..8c82a8e13b 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java @@ -28,7 +28,6 @@ public class UserEntity extends AbstractIdentifiableEntity { private List federatedIdentities; private String federationLink; private String serviceAccountClientLink; - private List offlineUserSessions; public String getUsername() { return username; @@ -158,13 +157,5 @@ public class UserEntity extends AbstractIdentifiableEntity { public void setServiceAccountClientLink(String serviceAccountClientLink) { this.serviceAccountClientLink = serviceAccountClientLink; } - - public List getOfflineUserSessions() { - return offlineUserSessions; - } - - public void setOfflineUserSessions(List offlineUserSessions) { - this.offlineUserSessions = offlineUserSessions; - } } diff --git a/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java new file mode 100644 index 0000000000..6daf7c749c --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java @@ -0,0 +1,104 @@ +package org.keycloak.models.session; + +import java.util.Collections; +import java.util.List; + +import org.keycloak.Config; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; + +/** + * Persistence of userSessions is disabled . Useful just if you never need survive of userSessions/clientSessions + * among server restart. Offline sessions / offline tokens will be invalid after server restart as well, + * + * @author Marek Posolda + */ +public class DisabledUserSessionPersisterProvider implements UserSessionPersisterProviderFactory, UserSessionPersisterProvider { + + public static final String ID = "disabled"; + + @Override + public UserSessionPersisterProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return ID; + } + + @Override + public void createUserSession(UserSessionModel userSession, boolean offline) { + + } + + @Override + public void createClientSession(ClientSessionModel clientSession, boolean offline) { + + } + + @Override + public void updateUserSession(UserSessionModel userSession, boolean offline) { + + } + + @Override + public void removeUserSession(String userSessionId, boolean offline) { + + } + + @Override + public void removeClientSession(String clientSessionId, boolean offline) { + + } + + @Override + public void onRealmRemoved(RealmModel realm) { + + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + + } + + @Override + public void onUserRemoved(RealmModel realm, UserModel user) { + + } + + @Override + public void clearDetachedUserSessions() { + + } + + @Override + public List loadUserSessions(int firstResult, int maxResults, boolean offline) { + return Collections.emptyList(); + } + + @Override + public int getUserSessionsCount(boolean offline) { + return 0; + } +} diff --git a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java new file mode 100644 index 0000000000..1fced88bbe --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java @@ -0,0 +1,401 @@ +package org.keycloak.models.session; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.util.JsonSerialization; + +/** + * @author Marek Posolda + */ +public class PersistentClientSessionAdapter implements ClientSessionModel { + + private final PersistentClientSessionModel model; + private final RealmModel realm; + private final ClientModel client; + private UserSessionModel userSession; + + private PersistentClientSessionData data; + + public PersistentClientSessionAdapter(ClientSessionModel clientSession) { + data = new PersistentClientSessionData(); + data.setAction(clientSession.getAction()); + data.setAuthMethod(clientSession.getAuthMethod()); + data.setExecutionStatus(clientSession.getExecutionStatus()); + data.setNotes(clientSession.getNotes()); + data.setProtocolMappers(clientSession.getProtocolMappers()); + data.setRedirectUri(clientSession.getRedirectUri()); + data.setRoles(clientSession.getRoles()); + data.setTimestamp(clientSession.getTimestamp()); + data.setUserSessionNotes(clientSession.getUserSessionNotes()); + + model = new PersistentClientSessionModel(); + model.setClientId(clientSession.getClient().getId()); + model.setClientSessionId(clientSession.getId()); + if (clientSession.getAuthenticatedUser() != null) { + model.setUserId(clientSession.getAuthenticatedUser().getId()); + } + model.setUserSessionId(clientSession.getUserSession().getId()); + + realm = clientSession.getRealm(); + client = clientSession.getClient(); + userSession = clientSession.getUserSession(); + } + + public PersistentClientSessionAdapter(PersistentClientSessionModel model, RealmModel realm, ClientModel client, UserSessionModel userSession) { + this.model = model; + this.realm = realm; + this.client = client; + this.userSession = userSession; + } + + // Lazily init data + private PersistentClientSessionData getData() { + if (data == null) { + try { + data = JsonSerialization.readValue(model.getData(), PersistentClientSessionData.class); + } catch (IOException ioe) { + throw new ModelException(ioe); + } + } + + return data; + } + + // Write updated model with latest serialized data + public PersistentClientSessionModel getUpdatedModel() { + try { + String updatedData = JsonSerialization.writeValueAsString(getData()); + this.model.setData(updatedData); + } catch (IOException ioe) { + throw new ModelException(ioe); + } + + return this.model; + } + + @Override + public String getId() { + return model.getClientSessionId(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public ClientModel getClient() { + return client; + } + + @Override + public UserSessionModel getUserSession() { + return userSession; + } + + @Override + public void setUserSession(UserSessionModel userSession) { + this.userSession = userSession; + } + + @Override + public String getRedirectUri() { + return getData().getRedirectUri(); + } + + @Override + public void setRedirectUri(String uri) { + getData().setRedirectUri(uri); + } + + @Override + public int getTimestamp() { + return getData().getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + getData().setTimestamp(timestamp); + } + + @Override + public String getAction() { + return getData().getAction(); + } + + @Override + public void setAction(String action) { + getData().setAction(action); + } + + @Override + public Set getRoles() { + return getData().getRoles(); + } + + @Override + public void setRoles(Set roles) { + getData().setRoles(roles); + } + + @Override + public Set getProtocolMappers() { + return getData().getProtocolMappers(); + } + + @Override + public void setProtocolMappers(Set protocolMappers) { + getData().setProtocolMappers(protocolMappers); + } + + @Override + public Map getExecutionStatus() { + return getData().getExecutionStatus(); + } + + @Override + public void setExecutionStatus(String authenticator, ExecutionStatus status) { + getData().getExecutionStatus().put(authenticator, status); + } + + @Override + public void clearExecutionStatus() { + getData().getExecutionStatus().clear(); + } + + @Override + public UserModel getAuthenticatedUser() { + return userSession.getUser(); + } + + @Override + public void setAuthenticatedUser(UserModel user) { + throw new IllegalStateException("Not supported setAuthenticatedUser"); + } + + @Override + public String getAuthMethod() { + return getData().getAuthMethod(); + } + + @Override + public void setAuthMethod(String method) { + getData().setAuthMethod(method); + } + + @Override + public String getNote(String name) { + PersistentClientSessionData entity = getData(); + return entity.getNotes()==null ? null : entity.getNotes().get(name); + } + + @Override + public void setNote(String name, String value) { + PersistentClientSessionData entity = getData(); + if (entity.getNotes() == null) { + entity.setNotes(new HashMap()); + } + entity.getNotes().put(name, value); + } + + @Override + public void removeNote(String name) { + PersistentClientSessionData entity = getData(); + if (entity.getNotes() != null) { + entity.getNotes().remove(name); + } + } + + @Override + public Map getNotes() { + PersistentClientSessionData entity = getData(); + if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap(); + return entity.getNotes(); + } + + @Override + public Set getRequiredActions() { + return getData().getRequiredActions(); + } + + @Override + public void addRequiredAction(String action) { + getData().getRequiredActions().add(action); + } + + @Override + public void removeRequiredAction(String action) { + getData().getRequiredActions().remove(action); + } + + @Override + public void addRequiredAction(UserModel.RequiredAction action) { + addRequiredAction(action.name()); + } + + @Override + public void removeRequiredAction(UserModel.RequiredAction action) { + removeRequiredAction(action.name()); + } + + @Override + public void setUserSessionNote(String name, String value) { + PersistentClientSessionData entity = getData(); + if (entity.getUserSessionNotes() == null) { + entity.setUserSessionNotes(new HashMap()); + } + entity.getUserSessionNotes().put(name, value); + } + + @Override + public Map getUserSessionNotes() { + PersistentClientSessionData entity = getData(); + if (entity.getUserSessionNotes() == null || entity.getUserSessionNotes().isEmpty()) return Collections.emptyMap(); + return entity.getUserSessionNotes(); + } + + @Override + public void clearUserSessionNotes() { + PersistentClientSessionData entity = getData(); + entity.setUserSessionNotes(new HashMap()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof ClientSessionModel)) return false; + + ClientSessionModel that = (ClientSessionModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + protected static class PersistentClientSessionData { + + @JsonProperty("authMethod") + private String authMethod; + + @JsonProperty("redirectUri") + private String redirectUri; + + @JsonProperty("protocolMappers") + private Set protocolMappers; + + @JsonProperty("roles") + private Set roles; + + @JsonProperty("notes") + private Map notes; + + @JsonProperty("userSessionNotes") + private Map userSessionNotes; + + @JsonProperty("executionStatus") + private Map executionStatus = new HashMap<>(); + + @JsonProperty("timestamp") + private int timestamp; + + @JsonProperty("action") + private String action; + + @JsonProperty("requiredActions") + private Set requiredActions = new HashSet<>(); + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public Set getProtocolMappers() { + return protocolMappers; + } + + public void setProtocolMappers(Set protocolMappers) { + this.protocolMappers = protocolMappers; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public Map getNotes() { + return notes; + } + + public void setNotes(Map notes) { + this.notes = notes; + } + + public Map getUserSessionNotes() { + return userSessionNotes; + } + + public void setUserSessionNotes(Map userSessionNotes) { + this.userSessionNotes = userSessionNotes; + } + + public Map getExecutionStatus() { + return executionStatus; + } + + public void setExecutionStatus(Map executionStatus) { + this.executionStatus = executionStatus; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public Set getRequiredActions() { + return requiredActions; + } + + public void setRequiredActions(Set requiredActions) { + this.requiredActions = requiredActions; + } + } +} diff --git a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java similarity index 92% rename from model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java rename to model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java index 59e325d8d2..96e900fb7b 100644 --- a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java @@ -1,9 +1,9 @@ -package org.keycloak.models; +package org.keycloak.models.session; /** * @author Marek Posolda */ -public class OfflineClientSessionModel { +public class PersistentClientSessionModel { private String clientSessionId; private String userSessionId; diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java b/model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java similarity index 57% rename from services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java rename to model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index bd01d3884e..e5fc68c4aa 100644 --- a/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java +++ b/model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -1,13 +1,14 @@ -package org.keycloak.services.offline; +package org.keycloak.models.session; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.codehaus.jackson.annotate.JsonProperty; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ModelException; -import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.util.JsonSerialization; @@ -15,23 +16,47 @@ import org.keycloak.util.JsonSerialization; /** * @author Marek Posolda */ -public class OfflineUserSessionAdapter implements UserSessionModel { +public class PersistentUserSessionAdapter implements UserSessionModel { - private final OfflineUserSessionModel model; + private final PersistentUserSessionModel model; private final UserModel user; + private final RealmModel realm; + private final List clientSessions; - private OfflineUserSessionData data; + private PersistentUserSessionData data; - public OfflineUserSessionAdapter(OfflineUserSessionModel model, UserModel user) { - this.model = model; - this.user = user; + public PersistentUserSessionAdapter(UserSessionModel other) { + this.data = new PersistentUserSessionData(); + data.setAuthMethod(other.getAuthMethod()); + data.setBrokerSessionId(other.getBrokerSessionId()); + data.setBrokerUserId(other.getBrokerUserId()); + data.setIpAddress(other.getIpAddress()); + data.setNotes(other.getNotes()); + data.setRememberMe(other.isRememberMe()); + data.setStarted(other.getStarted()); + data.setState(other.getState()); + + this.model = new PersistentUserSessionModel(); + this.model.setUserSessionId(other.getId()); + this.model.setLastSessionRefresh(other.getLastSessionRefresh()); + + this.user = other.getUser(); + this.realm = other.getRealm(); + this.clientSessions = other.getClientSessions(); } - // lazily init representation - private OfflineUserSessionData getData() { + public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, List clientSessions) { + this.model = model; + this.realm = realm; + this.user = user; + this.clientSessions = clientSessions; + } + + // Lazily init data + private PersistentUserSessionData getData() { if (data == null) { try { - data = JsonSerialization.readValue(model.getData(), OfflineUserSessionData.class); + data = JsonSerialization.readValue(model.getData(), PersistentUserSessionData.class); } catch (IOException ioe) { throw new ModelException(ioe); } @@ -40,6 +65,18 @@ public class OfflineUserSessionAdapter implements UserSessionModel { return data; } + // Write updated model with latest serialized data + public PersistentUserSessionModel getUpdatedModel() { + try { + String updatedData = JsonSerialization.writeValueAsString(getData()); + this.model.setData(updatedData); + } catch (IOException ioe) { + throw new ModelException(ioe); + } + + return this.model; + } + @Override public String getId() { return model.getUserSessionId(); @@ -60,6 +97,11 @@ public class OfflineUserSessionAdapter implements UserSessionModel { return user; } + @Override + public RealmModel getRealm() { + return realm; + } + @Override public String getLoginUsername() { return user.getUsername(); @@ -87,17 +129,17 @@ public class OfflineUserSessionAdapter implements UserSessionModel { @Override public int getLastSessionRefresh() { - return 0; + return model.getLastSessionRefresh(); } @Override public void setLastSessionRefresh(int seconds) { - // Ignore + model.setLastSessionRefresh(seconds); } @Override public List getClientSessions() { - throw new IllegalStateException("Not yet supported"); + return clientSessions; } @Override @@ -107,13 +149,19 @@ public class OfflineUserSessionAdapter implements UserSessionModel { @Override public void setNote(String name, String value) { - throw new IllegalStateException("Illegal to set note offline session"); + PersistentUserSessionData data = getData(); + if (data.getNotes() == null) { + data.setNotes(new HashMap()); + } + data.getNotes().put(name, value); } @Override public void removeNote(String name) { - throw new IllegalStateException("Illegal to remove note from offline session"); + if (getData().getNotes() != null) { + getData().getNotes().remove(name); + } } @Override @@ -123,16 +171,29 @@ public class OfflineUserSessionAdapter implements UserSessionModel { @Override public State getState() { - return null; + return getData().getState(); } @Override public void setState(State state) { - throw new IllegalStateException("Illegal to set state on offline session"); + getData().setState(state); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof UserSessionModel)) return false; - protected static class OfflineUserSessionData { + UserSessionModel that = (UserSessionModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + protected static class PersistentUserSessionData { @JsonProperty("brokerSessionId") private String brokerSessionId; @@ -155,6 +216,9 @@ public class OfflineUserSessionAdapter implements UserSessionModel { @JsonProperty("notes") private Map notes; + @JsonProperty("state") + private State state; + public String getBrokerSessionId() { return brokerSessionId; } @@ -211,5 +275,12 @@ public class OfflineUserSessionAdapter implements UserSessionModel { this.notes = notes; } + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } } } diff --git a/model/api/src/main/java/org/keycloak/models/OfflineUserSessionModel.java b/model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java similarity index 58% rename from model/api/src/main/java/org/keycloak/models/OfflineUserSessionModel.java rename to model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java index 9907783899..dc14e98173 100644 --- a/model/api/src/main/java/org/keycloak/models/OfflineUserSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java @@ -1,11 +1,13 @@ -package org.keycloak.models; +package org.keycloak.models.session; /** * @author Marek Posolda */ -public class OfflineUserSessionModel { +public class PersistentUserSessionModel { private String userSessionId; + private int lastSessionRefresh; + private String data; public String getUserSessionId() { @@ -16,6 +18,15 @@ public class OfflineUserSessionModel { this.userSessionId = userSessionId; } + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + public void setLastSessionRefresh(int lastSessionRefresh) { + this.lastSessionRefresh = lastSessionRefresh; + } + + public String getData() { return data; } diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java new file mode 100644 index 0000000000..4b3355e9d3 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java @@ -0,0 +1,43 @@ +package org.keycloak.models.session; + +import java.util.List; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.provider.Provider; + +/** + * @author Marek Posolda + */ +public interface UserSessionPersisterProvider extends Provider { + + // Persist just userSession. Not it's clientSessions + void createUserSession(UserSessionModel userSession, boolean offline); + + // Assuming that corresponding userSession is already persisted + void createClientSession(ClientSessionModel clientSession, boolean offline); + + void updateUserSession(UserSessionModel userSession, boolean offline); + + // Called during logout (for online session) or during periodic expiration. It will remove all corresponding clientSessions too + void removeUserSession(String userSessionId, boolean offline); + + // Called during revoke. It will remove userSession too if this was last clientSession attached to it + void removeClientSession(String clientSessionId, boolean offline); + + void onRealmRemoved(RealmModel realm); + void onClientRemoved(RealmModel realm, ClientModel client); + void onUserRemoved(RealmModel realm, UserModel user); + + // Called at startup to remove userSessions without any clientSession + void clearDetachedUserSessions(); + + // Called during startup. For each userSession, it loads also clientSessions + List loadUserSessions(int firstResult, int maxResults, boolean offline); + + int getUserSessionsCount(boolean offline); + +} diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java new file mode 100644 index 0000000000..39e7b2e113 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java @@ -0,0 +1,9 @@ +package org.keycloak.models.session; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marek Posolda + */ +public interface UserSessionPersisterProviderFactory extends ProviderFactory { +} diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java new file mode 100644 index 0000000000..cb66fa2601 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java @@ -0,0 +1,31 @@ +package org.keycloak.models.session; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class UserSessionPersisterSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "userSessionPersister"; + } + + @Override + public Class getProviderClass() { + return UserSessionPersisterProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return UserSessionPersisterProviderFactory.class; + } +} diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index de062d58b5..96b47cb1a4 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -10,8 +10,8 @@ import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.ModelException; import org.keycloak.models.OTPPolicy; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -511,13 +511,13 @@ public class ModelToRepresentation { return rep; } - public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, OfflineUserSessionModel model, Collection clientSessions) { + public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, PersistentUserSessionModel model, Collection clientSessions) { OfflineUserSessionRepresentation rep = new OfflineUserSessionRepresentation(); rep.setData(model.getData()); rep.setUserSessionId(model.getUserSessionId()); List clientSessionReps = new LinkedList<>(); - for (OfflineClientSessionModel clsm : clientSessions) { + for (PersistentClientSessionModel clsm : clientSessions) { OfflineClientSessionRepresentation clrep = toRepresentation(realm, clsm); clientSessionReps.add(clrep); } @@ -525,7 +525,7 @@ public class ModelToRepresentation { return rep; } - public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, OfflineClientSessionModel model) { + public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, PersistentClientSessionModel model) { OfflineClientSessionRepresentation rep = new OfflineClientSessionRepresentation(); String clientInternalId = model.getClientId(); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 482918254d..62933a04c7 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -1,7 +1,7 @@ package org.keycloak.models.utils; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.representations.idm.OfflineClientSessionRepresentation; import org.keycloak.representations.idm.OfflineUserSessionRepresentation; import org.keycloak.util.Base64; @@ -985,11 +985,6 @@ public class RepresentationToModel { user.addConsent(consentModel); } } - if (userRep.getOfflineUserSessions() != null) { - for (OfflineUserSessionRepresentation sessionRep : userRep.getOfflineUserSessions()) { - importOfflineSession(session, newRealm, user, sessionRep); - } - } if (userRep.getServiceAccountClientId() != null) { String clientId = userRep.getServiceAccountClientId(); ClientModel client = clientMap.get(clientId); @@ -1160,28 +1155,29 @@ public class RepresentationToModel { return consentModel; } - public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) { - OfflineUserSessionModel model = new OfflineUserSessionModel(); - model.setUserSessionId(sessionRep.getUserSessionId()); - model.setData(sessionRep.getData()); - session.users().addOfflineUserSession(newRealm, user, model); - - for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) { - OfflineClientSessionModel csModel = new OfflineClientSessionModel(); - String clientId = csRep.getClient(); - ClientModel client = newRealm.getClientByClientId(clientId); - if (client == null) { - throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername()); - } - csModel.setClientId(client.getId()); - csModel.setUserId(user.getId()); - csModel.setClientSessionId(csRep.getClientSessionId()); - csModel.setUserSessionId(sessionRep.getUserSessionId()); - csModel.setData(csRep.getData()); - - session.users().addOfflineClientSession(newRealm, csModel); - } - } + // TODO +// public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) { +// PersistentUserSessionModel model = new PersistentUserSessionModel(); +// model.setUserSessionId(sessionRep.getUserSessionId()); +// model.setData(sessionRep.getData()); +// session.users().createOfflineUserSession(newRealm, user, model); +// +// for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) { +// PersistentClientSessionModel csModel = new PersistentClientSessionModel(); +// String clientId = csRep.getClient(); +// ClientModel client = newRealm.getClientByClientId(clientId); +// if (client == null) { +// throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername()); +// } +// csModel.setClientId(client.getId()); +// csModel.setUserId(user.getId()); +// csModel.setClientSessionId(csRep.getClientSessionId()); +// csModel.setUserSessionId(sessionRep.getUserSessionId()); +// csModel.setData(csRep.getData()); +// +// session.users().createOfflineClientSession(newRealm, csModel); +// } +// } public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) { AuthenticationFlowModel model = new AuthenticationFlowModel(); diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 5f7303129e..4cd162bce6 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -1,15 +1,12 @@ package org.keycloak.models.utils; import org.keycloak.models.ClientModel; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory b/model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory new file mode 100644 index 0000000000..dc21d4d333 --- /dev/null +++ b/model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory @@ -0,0 +1 @@ +org.keycloak.models.session.DisabledUserSessionPersisterProvider \ No newline at end of file diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi index ba45379b08..be3982bce4 100755 --- a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -3,4 +3,5 @@ org.keycloak.mappers.UserFederationMapperSpi org.keycloak.models.RealmSpi org.keycloak.models.UserSessionSpi org.keycloak.models.UserSpi +org.keycloak.models.session.UserSessionPersisterSpi org.keycloak.migration.MigrationSpi \ No newline at end of file diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index 9f4080ab70..8edfe3ec2e 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -24,8 +24,8 @@ import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -35,8 +35,8 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.entities.FederatedIdentityEntity; -import org.keycloak.models.entities.OfflineClientSessionEntity; -import org.keycloak.models.entities.OfflineUserSessionEntity; +import org.keycloak.models.entities.PersistentClientSessionEntity; +import org.keycloak.models.entities.PersistentUserSessionEntity; import org.keycloak.models.entities.UserEntity; import org.keycloak.models.file.adapter.UserAdapter; import org.keycloak.models.utils.CredentialValidation; @@ -494,188 +494,4 @@ public class FileUserProvider implements UserProvider { //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. return null; // not supported yet } - - @Override - public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) { - userModel = getUserById(userModel.getId(), realm); - UserEntity userEntity = ((UserAdapter) userModel).getUserEntity(); - - if (userEntity.getOfflineUserSessions() == null) { - userEntity.setOfflineUserSessions(new ArrayList()); - } - - if (getUserSessionEntityById(userEntity, userSession.getUserSessionId()) != null) { - throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + userEntity.getUsername()); - } - - OfflineUserSessionEntity entity = new OfflineUserSessionEntity(); - entity.setUserSessionId(userSession.getUserSessionId()); - entity.setData(userSession.getData()); - entity.setOfflineClientSessions(new ArrayList()); - userEntity.getOfflineUserSessions().add(entity); - } - - @Override - public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) { - userModel = getUserById(userModel.getId(), realm); - UserEntity userEntity = ((UserAdapter) userModel).getUserEntity(); - - OfflineUserSessionEntity entity = getUserSessionEntityById(userEntity, userSessionId); - return entity==null ? null : toModel(entity); - } - - @Override - public Collection getOfflineUserSessions(RealmModel realm, UserModel userModel) { - userModel = getUserById(userModel.getId(), realm); - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - - if (user.getOfflineUserSessions()==null) { - return Collections.emptyList(); - } else { - List result = new ArrayList<>(); - for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) { - result.add(toModel(entity)); - } - return result; - } - } - - private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) { - OfflineUserSessionModel model = new OfflineUserSessionModel(); - model.setUserSessionId(entity.getUserSessionId()); - model.setData(entity.getData()); - return model; - } - - @Override - public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) { - userModel = getUserById(userModel.getId(), realm); - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - - OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId); - if (entity != null) { - user.getOfflineUserSessions().remove(entity); - return true; - } else { - return false; - } - } - - private OfflineUserSessionEntity getUserSessionEntityById(UserEntity user, String userSessionId) { - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) { - if (entity.getUserSessionId().equals(userSessionId)) { - return entity; - } - } - } - return null; - } - - @Override - public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) { - UserModel userModel = getUserById(clientSession.getUserId(), realm); - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - - OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId()); - if (userSessionEntity == null) { - throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername()); - } - - OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity(); - clEntity.setClientSessionId(clientSession.getClientSessionId()); - clEntity.setClientId(clientSession.getClientId()); - clEntity.setData(clientSession.getData()); - - userSessionEntity.getOfflineClientSessions().add(clEntity); - } - - @Override - public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) { - userModel = getUserById(userModel.getId(), realm); - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - if (clSession.getClientSessionId().equals(clientSessionId)) { - return toModel(clSession, userSession.getUserSessionId()); - } - } - } - } - - return null; - } - - private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) { - OfflineClientSessionModel model = new OfflineClientSessionModel(); - model.setClientSessionId(cls.getClientSessionId()); - model.setClientId(cls.getClientId()); - model.setData(cls.getData()); - model.setUserSessionId(userSessionId); - return model; - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, UserModel userModel) { - userModel = getUserById(userModel.getId(), realm); - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - - List result = new ArrayList<>(); - - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - result.add(toModel(clSession, userSession.getUserSessionId())); - } - } - } - - return result; - } - - @Override - public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) { - userModel = getUserById(userModel.getId(), realm); - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - if (clSession.getClientSessionId().equals(clientSessionId)) { - userSession.getOfflineClientSessions().remove(clSession); - return true; - } - } - } - } - - return false; - } - - @Override - public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) { - return getOfflineClientSessions(realm, client, -1, -1).size(); - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - List result = new LinkedList<>(); - - List users = new ArrayList<>(inMemoryModel.getUsers(realm.getId())); - users = sortedSubList(users, firstResult, maxResults); - - for (UserModel userModel : users) { - UserEntity user = ((UserAdapter) userModel).getUserEntity(); - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - if (clSession.getClientId().equals(client.getId())) { - result.add(toModel(clSession, userSession.getUserSessionId())); - } - } - } - } - return result; - } } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index 2c233d1068..a4442c8029 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -22,10 +22,7 @@ import org.keycloak.models.ClientModel; import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.ModelException; import org.keycloak.models.OTPPolicy; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; @@ -35,8 +32,6 @@ import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.entities.CredentialEntity; import org.keycloak.models.entities.FederatedIdentityEntity; -import org.keycloak.models.entities.OfflineClientSessionEntity; -import org.keycloak.models.entities.OfflineUserSessionEntity; import org.keycloak.models.entities.RoleEntity; import org.keycloak.models.entities.UserEntity; import org.keycloak.models.utils.KeycloakModelUtils; @@ -44,7 +39,6 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.util.Time; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -53,8 +47,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; - /** * UserModel for JSON persistence. * diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java index 584e91f764..69fc5cf577 100644 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java @@ -4,6 +4,8 @@ import org.keycloak.models.*; import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.entities.CachedUser; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionModel; import java.util.*; @@ -121,7 +123,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider { if (model == null) return null; if (managedUsers.containsKey(id)) return managedUsers.get(id); if (userInvalidations.containsKey(id)) return model; - cached = new CachedUser(this, realm, model); + cached = new CachedUser(realm, model); cache.addCachedUser(realm.getId(), cached); } else if (managedUsers.containsKey(id)) { return managedUsers.get(id); @@ -146,7 +148,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider { if (model == null) return null; if (managedUsers.containsKey(model.getId())) return managedUsers.get(model.getId()); if (userInvalidations.containsKey(model.getId())) return model; - cached = new CachedUser(this, realm, model); + cached = new CachedUser(realm, model); cache.addCachedUser(realm.getId(), cached); } else if (userInvalidations.containsKey(cached.getId())) { return getDelegate().getUserById(cached.getId(), realm); @@ -173,7 +175,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider { UserModel model = getDelegate().getUserByEmail(email, realm); if (model == null) return null; if (userInvalidations.containsKey(model.getId())) return model; - cached = new CachedUser(this, realm, model); + cached = new CachedUser(realm, model); cache.addCachedUser(realm.getId(), cached); } else if (userInvalidations.containsKey(cached.getId())) { return getDelegate().getUserByEmail(email, realm); @@ -328,94 +330,4 @@ public class DefaultCacheUserProvider implements CacheUserProvider { public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) { getDelegate().preRemove(client, protocolMapper); } - - @Override - public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) { - registerUserInvalidation(realm, user.getId()); - getDelegate().addOfflineUserSession(realm, user, offlineUserSession); - } - - @Override - public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) { - if (isRegisteredForInvalidation(realm, user.getId())) { - return getDelegate().getOfflineUserSession(realm, user, userSessionId); - } - - CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId()); - if (cachedUser == null) { - return getDelegate().getOfflineUserSession(realm, user, userSessionId); - } else { - return cachedUser.getOfflineUserSessions().get(userSessionId); - } - } - - @Override - public Collection getOfflineUserSessions(RealmModel realm, UserModel user) { - if (isRegisteredForInvalidation(realm, user.getId())) { - return getDelegate().getOfflineUserSessions(realm, user); - } - - CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId()); - if (cachedUser == null) { - return getDelegate().getOfflineUserSessions(realm, user); - } else { - return cachedUser.getOfflineUserSessions().values(); - } - } - - @Override - public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) { - registerUserInvalidation(realm, user.getId()); - return getDelegate().removeOfflineUserSession(realm, user, userSessionId); - } - - @Override - public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) { - registerUserInvalidation(realm, offlineClientSession.getUserId()); - getDelegate().addOfflineClientSession(realm, offlineClientSession); - } - - @Override - public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) { - if (isRegisteredForInvalidation(realm, user.getId())) { - return getDelegate().getOfflineClientSession(realm, user, clientSessionId); - } - - CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId()); - if (cachedUser == null) { - return getDelegate().getOfflineClientSession(realm, user, clientSessionId); - } else { - return cachedUser.getOfflineClientSessions().get(clientSessionId); - } - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, UserModel user) { - if (isRegisteredForInvalidation(realm, user.getId())) { - return getDelegate().getOfflineClientSessions(realm, user); - } - - CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId()); - if (cachedUser == null) { - return getDelegate().getOfflineClientSessions(realm, user); - } else { - return cachedUser.getOfflineClientSessions().values(); - } - } - - @Override - public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) { - registerUserInvalidation(realm, user.getId()); - return getDelegate().removeOfflineClientSession(realm, user, clientSessionId); - } - - @Override - public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) { - return getDelegate().getOfflineClientSessionsCount(realm, client); - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - return getDelegate().getOfflineClientSessions(realm, client, firstResult, maxResults); - } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 24e866a4ae..0083856d14 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -1,20 +1,15 @@ package org.keycloak.models.cache.entities; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; -import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.util.MultivaluedHashMap; import java.io.Serializable; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -30,18 +25,16 @@ public class CachedUser implements Serializable { private String lastName; private String email; private boolean emailVerified; - private List credentials = new LinkedList(); + private List credentials = new LinkedList<>(); private boolean enabled; private boolean totp; private String federationLink; private String serviceAccountClientLink; private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); private Set requiredActions = new HashSet<>(); - private Set roleMappings = new HashSet(); - private Map offlineUserSessions = new HashMap<>(); - private Map offlineClientSessions = new HashMap<>(); + private Set roleMappings = new HashSet<>(); - public CachedUser(CacheUserProvider cacheUserProvider, RealmModel realm, UserModel user) { + public CachedUser(RealmModel realm, UserModel user) { this.id = user.getId(); this.realm = realm.getId(); this.username = user.getUsername(); @@ -60,12 +53,6 @@ public class CachedUser implements Serializable { for (RoleModel role : user.getRoleMappings()) { roleMappings.add(role.getId()); } - for (OfflineUserSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineUserSessions(realm, user)) { - offlineUserSessions.put(offlineSession.getUserSessionId(), offlineSession); - } - for (OfflineClientSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineClientSessions(realm, user)) { - offlineClientSessions.put(offlineSession.getClientSessionId(), offlineSession); - } } public String getId() { @@ -131,12 +118,4 @@ public class CachedUser implements Serializable { public String getServiceAccountClientLink() { return serviceAccountClientLink; } - - public Map getOfflineUserSessions() { - return offlineUserSessions; - } - - public Map getOfflineClientSessions() { - return offlineClientSessions; - } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakTransaction.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakTransaction.java deleted file mode 100755 index f19cffa84d..0000000000 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakTransaction.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.keycloak.models.jpa; - -import org.keycloak.models.KeycloakTransaction; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceException; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JpaKeycloakTransaction implements KeycloakTransaction { - - protected EntityManager em; - - public JpaKeycloakTransaction(EntityManager em) { - this.em = em; - } - - @Override - public void begin() { - em.getTransaction().begin(); - } - - @Override - public void commit() { - try { - em.getTransaction().commit(); - } catch (PersistenceException e) { - throw PersistenceExceptionConverter.convert(e.getCause() != null ? e.getCause() : e); - } - } - - @Override - public void rollback() { - em.getTransaction().rollback(); - } - - @Override - public void setRollbackOnly() { - em.getTransaction().setRollbackOnly(); - } - - @Override - public boolean getRollbackOnly() { - return em.getTransaction().getRollbackOnly(); - } - - @Override - public boolean isActive() { - return em.getTransaction().isActive(); - } -} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 563e89866b..d4d533a9dc 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -4,8 +4,6 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -15,21 +13,15 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.jpa.entities.FederatedIdentityEntity; -import org.keycloak.models.jpa.entities.OfflineClientSessionEntity; -import org.keycloak.models.jpa.entities.OfflineUserSessionEntity; import org.keycloak.models.jpa.entities.UserAttributeEntity; import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; -import javax.persistence.Query; import javax.persistence.TypedQuery; import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -175,10 +167,6 @@ public class JpaUserProvider implements UserProvider { .setParameter("realmId", realm.getId()).executeUpdate(); num = em.createNamedQuery("deleteUserAttributesByRealm") .setParameter("realmId", realm.getId()).executeUpdate(); - num = em.createNamedQuery("deleteOfflineClientSessionsByRealm") - .setParameter("realmId", realm.getId()).executeUpdate(); - num = em.createNamedQuery("deleteOfflineUserSessionsByRealm") - .setParameter("realmId", realm.getId()).executeUpdate(); num = em.createNamedQuery("deleteUsersByRealm") .setParameter("realmId", realm.getId()).executeUpdate(); } @@ -205,14 +193,6 @@ public class JpaUserProvider implements UserProvider { .setParameter("realmId", realm.getId()) .setParameter("link", link.getId()) .executeUpdate(); - num = em.createNamedQuery("deleteOfflineClientSessionsByRealmAndLink") - .setParameter("realmId", realm.getId()) - .setParameter("link", link.getId()) - .executeUpdate(); - num = em.createNamedQuery("deleteOfflineUserSessionsByRealmAndLink") - .setParameter("realmId", realm.getId()) - .setParameter("link", link.getId()) - .executeUpdate(); num = em.createNamedQuery("deleteUsersByRealmAndLink") .setParameter("realmId", realm.getId()) .setParameter("link", link.getId()) @@ -230,8 +210,6 @@ public class JpaUserProvider implements UserProvider { em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate(); em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate(); em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteOfflineClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteDetachedOfflineUserSessions").executeUpdate(); } @Override @@ -479,167 +457,4 @@ public class JpaUserProvider implements UserProvider { // Not supported yet return null; } - - @Override - public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - OfflineUserSessionEntity entity = new OfflineUserSessionEntity(); - entity.setUser(userEntity); - entity.setUserSessionId(offlineUserSession.getUserSessionId()); - entity.setData(offlineUserSession.getData()); - em.persist(entity); - userEntity.getOfflineUserSessions().add(entity); - em.flush(); - } - - @Override - public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) { - if (entity.getUserSessionId().equals(userSessionId)) { - return toModel(entity); - } - } - return null; - } - - private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) { - OfflineUserSessionModel model = new OfflineUserSessionModel(); - model.setUserSessionId(entity.getUserSessionId()); - model.setData(entity.getData()); - return model; - } - - @Override - public Collection getOfflineUserSessions(RealmModel realm, UserModel user) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - List result = new LinkedList<>(); - for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) { - result.add(toModel(entity)); - } - return result; - } - - @Override - public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - OfflineUserSessionEntity found = null; - for (OfflineUserSessionEntity session : userEntity.getOfflineUserSessions()) { - if (session.getUserSessionId().equals(userSessionId)) { - found = session; - break; - } - } - - if (found == null) { - return false; - } else { - userEntity.getOfflineUserSessions().remove(found); - em.remove(found); - em.flush(); - return true; - } - } - - @Override - public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) { - UserEntity userEntity = em.getReference(UserEntity.class, offlineClientSession.getUserId()); - - OfflineClientSessionEntity entity = new OfflineClientSessionEntity(); - entity.setUser(userEntity); - entity.setClientSessionId(offlineClientSession.getClientSessionId()); - entity.setUserSessionId(offlineClientSession.getUserSessionId()); - entity.setClientId(offlineClientSession.getClientId()); - entity.setData(offlineClientSession.getData()); - em.persist(entity); - userEntity.getOfflineClientSessions().add(entity); - em.flush(); - } - - @Override - public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) { - if (entity.getClientSessionId().equals(clientSessionId)) { - return toModel(entity); - } - } - return null; - } - - private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) { - OfflineClientSessionModel model = new OfflineClientSessionModel(); - model.setClientSessionId(entity.getClientSessionId()); - model.setClientId(entity.getClientId()); - model.setUserId(entity.getUser().getId()); - model.setUserSessionId(entity.getUserSessionId()); - model.setData(entity.getData()); - return model; - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, UserModel user) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - List result = new LinkedList<>(); - for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) { - result.add(toModel(entity)); - } - return result; - } - - @Override - public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) { - UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); - - OfflineClientSessionEntity found = null; - for (OfflineClientSessionEntity session : userEntity.getOfflineClientSessions()) { - if (session.getClientSessionId().equals(clientSessionId)) { - found = session; - break; - } - } - - if (found == null) { - return false; - } else { - userEntity.getOfflineClientSessions().remove(found); - em.remove(found); - em.flush(); - return true; - } - } - - @Override - public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) { - Query query = em.createNamedQuery("findOfflineClientSessionsCountByClient"); - query.setParameter("clientId", client.getId()); - Number n = (Number) query.getSingleResult(); - return n.intValue(); - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - TypedQuery query = em.createNamedQuery("findOfflineClientSessionsByClient", OfflineClientSessionEntity.class); - query.setParameter("clientId", client.getId()); - - if (firstResult != -1) { - query.setFirstResult(firstResult); - } - if (maxResults != -1) { - query.setMaxResults(maxResults); - } - - List results = query.getResultList(); - Set set = new HashSet<>(); - for (OfflineClientSessionEntity entity : results) { - set.add(toModel(entity)); - } - return set; - } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 9c75057e4d..9757d5bb53 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -2,8 +2,6 @@ package org.keycloak.models.jpa; import org.keycloak.models.ClientModel; import org.keycloak.models.OTPPolicy; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.ModelDuplicateException; @@ -16,8 +14,6 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.jpa.entities.CredentialEntity; -import org.keycloak.models.jpa.entities.OfflineClientSessionEntity; -import org.keycloak.models.jpa.entities.OfflineUserSessionEntity; import org.keycloak.models.jpa.entities.UserConsentEntity; import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity; import org.keycloak.models.jpa.entities.UserConsentRoleEntity; @@ -37,11 +33,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java deleted file mode 100644 index e1c636a36f..0000000000 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.keycloak.models.jpa.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; - -/** - * @author Marek Posolda - */ -@NamedQueries({ - @NamedQuery(name="deleteOfflineClientSessionsByRealm", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"), - @NamedQuery(name="deleteOfflineClientSessionsByRealmAndLink", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), - @NamedQuery(name="deleteOfflineClientSessionsByClient", query="delete from OfflineClientSessionEntity sess where sess.clientId=:clientId"), - @NamedQuery(name="findOfflineClientSessionsCountByClient", query="select count(sess) from OfflineClientSessionEntity sess where sess.clientId=:clientId"), - @NamedQuery(name="findOfflineClientSessionsByClient", query="select sess from OfflineClientSessionEntity sess where sess.clientId=:clientId order by sess.user.username") -}) -@Table(name="OFFLINE_CLIENT_SESSION") -@Entity -public class OfflineClientSessionEntity { - - @Id - @Column(name="CLIENT_SESSION_ID", length = 36) - protected String clientSessionId; - - @Column(name="USER_SESSION_ID", length = 36) - protected String userSessionId; - - @Column(name="CLIENT_ID", length = 36) - protected String clientId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="USER_ID") - protected UserEntity user; - - @Column(name="DATA") - protected String data; - - public String getClientSessionId() { - return clientSessionId; - } - - public void setClientSessionId(String clientSessionId) { - this.clientSessionId = clientSessionId; - } - - public String getUserSessionId() { - return userSessionId; - } - - public void setUserSessionId(String userSessionId) { - this.userSessionId = userSessionId; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public UserEntity getUser() { - return user; - } - - public void setUser(UserEntity user) { - this.user = user; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } -} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java deleted file mode 100644 index d2726fa674..0000000000 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.keycloak.models.jpa.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; - -/** - * @author Marek Posolda - */ -@NamedQueries({ - @NamedQuery(name="deleteOfflineUserSessionsByRealm", query="delete from OfflineUserSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"), - @NamedQuery(name="deleteOfflineUserSessionsByRealmAndLink", query="delete from OfflineUserSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), - @NamedQuery(name="deleteDetachedOfflineUserSessions", query="delete from OfflineUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from OfflineClientSessionEntity c)") -}) -@Table(name="OFFLINE_USER_SESSION") -@Entity -public class OfflineUserSessionEntity { - - @Id - @Column(name="USER_SESSION_ID", length = 36) - protected String userSessionId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="USER_ID") - protected UserEntity user; - - @Column(name="DATA") - protected String data; - - public String getUserSessionId() { - return userSessionId; - } - - public void setUserSessionId(String userSessionId) { - this.userSessionId = userSessionId; - } - - public UserEntity getUser() { - return user; - } - - public void setUser(UserEntity user) { - this.user = user; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } -} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index 24d23dbb93..2da1641586 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -3,13 +3,9 @@ package org.keycloak.models.jpa.entities; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.CascadeType; -import javax.persistence.CollectionTable; import javax.persistence.Column; -import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.MapKeyColumn; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; @@ -18,8 +14,6 @@ import javax.persistence.UniqueConstraint; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; /** * @author Bill Burke @@ -89,12 +83,6 @@ public class UserEntity { @Column(name="SERVICE_ACCOUNT_CLIENT_LINK") protected String serviceAccountClientLink; - @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user") - protected Collection offlineUserSessions = new ArrayList<>(); - - @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user") - protected Collection offlineClientSessions = new ArrayList<>(); - public String getId() { return id; } @@ -224,22 +212,6 @@ public class UserEntity { this.serviceAccountClientLink = serviceAccountClientLink; } - public Collection getOfflineUserSessions() { - return offlineUserSessions; - } - - public void setOfflineUserSessions(Collection offlineUserSessions) { - this.offlineUserSessions = offlineUserSessions; - } - - public Collection getOfflineClientSessions() { - return offlineClientSessions; - } - - public void setOfflineClientSessions(Collection offlineClientSessions) { - this.offlineClientSessions = offlineClientSessions; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java new file mode 100644 index 0000000000..ffc04557b1 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -0,0 +1,239 @@ +package org.keycloak.models.jpa.session; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.TypedQuery; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.session.PersistentClientSessionAdapter; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionAdapter; +import org.keycloak.models.session.PersistentUserSessionModel; +import org.keycloak.models.session.UserSessionPersisterProvider; + +/** + * @author Marek Posolda + */ +public class JpaUserSessionPersisterProvider implements UserSessionPersisterProvider { + + private final KeycloakSession session; + private final EntityManager em; + + public JpaUserSessionPersisterProvider(KeycloakSession session, EntityManager em) { + this.session = session; + this.em = em; + } + + @Override + public void createUserSession(UserSessionModel userSession, boolean offline) { + PersistentUserSessionAdapter adapter = new PersistentUserSessionAdapter(userSession); + PersistentUserSessionModel model = adapter.getUpdatedModel(); + + PersistentUserSessionEntity entity = new PersistentUserSessionEntity(); + entity.setUserSessionId(model.getUserSessionId()); + entity.setRealmId(adapter.getRealm().getId()); + entity.setUserId(adapter.getUser().getId()); + entity.setOffline(offline); + entity.setLastSessionRefresh(model.getLastSessionRefresh()); + entity.setData(model.getData()); + em.persist(entity); + em.flush(); + } + + @Override + public void createClientSession(ClientSessionModel clientSession, boolean offline) { + PersistentClientSessionAdapter adapter = new PersistentClientSessionAdapter(clientSession); + PersistentClientSessionModel model = adapter.getUpdatedModel(); + + PersistentClientSessionEntity entity = new PersistentClientSessionEntity(); + entity.setClientSessionId(clientSession.getId()); + entity.setClientId(clientSession.getClient().getId()); + entity.setOffline(offline); + entity.setUserSessionId(clientSession.getUserSession().getId()); + entity.setData(model.getData()); + em.persist(entity); + em.flush(); + } + + @Override + public void updateUserSession(UserSessionModel userSession, boolean offline) { + PersistentUserSessionAdapter adapter; + if (userSession instanceof PersistentUserSessionAdapter) { + adapter = (PersistentUserSessionAdapter) userSession; + } else { + adapter = new PersistentUserSessionAdapter(userSession); + } + + PersistentUserSessionModel model = adapter.getUpdatedModel(); + + PersistentUserSessionEntity entity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(userSession.getId(), offline)); + if (entity == null) { + throw new ModelException("UserSession with ID " + userSession.getId() + ", offline: " + offline + " not found"); + } + entity.setLastSessionRefresh(model.getLastSessionRefresh()); + entity.setData(model.getData()); + } + + @Override + public void removeUserSession(String userSessionId, boolean offline) { + em.createNamedQuery("deleteClientSessionsByUserSession") + .setParameter("userSessionId", userSessionId) + .setParameter("offline", offline) + .executeUpdate(); + + PersistentUserSessionEntity sessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(userSessionId, offline)); + if (sessionEntity != null) { + em.remove(sessionEntity); + em.flush(); + } + } + + @Override + public void removeClientSession(String clientSessionId, boolean offline) { + PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(clientSessionId, offline)); + if (sessionEntity != null) { + em.remove(sessionEntity); + + // Remove userSession if it was last clientSession + List clientSessions = getClientSessionsByUserSession(sessionEntity.getUserSessionId(), offline); + if (clientSessions.size() == 0) { + PersistentUserSessionEntity userSessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(sessionEntity.getUserSessionId(), offline)); + if (userSessionEntity != null) { + em.remove(userSessionEntity); + } + } + + em.flush(); + } + } + + private List getClientSessionsByUserSession(String userSessionId, boolean offline) { + TypedQuery query = em.createNamedQuery("findClientSessionsByUserSession", PersistentClientSessionEntity.class); + query.setParameter("userSessionId", userSessionId); + query.setParameter("offline", offline); + return query.getResultList(); + } + + + + @Override + public void onRealmRemoved(RealmModel realm) { + em.createNamedQuery("deleteClientSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + em.createNamedQuery("deleteUserSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate(); + em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); + } + + @Override + public void onUserRemoved(RealmModel realm, UserModel user) { + em.createNamedQuery("deleteClientSessionsByUser").setParameter("userId", user.getId()).executeUpdate(); + em.createNamedQuery("deleteUserSessionsByUser").setParameter("userId", user.getId()).executeUpdate(); + } + + @Override + public void clearDetachedUserSessions() { + em.createNamedQuery("deleteDetachedClientSessions").executeUpdate(); + em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); + } + + @Override + public List loadUserSessions(int firstResult, int maxResults, boolean offline) { + TypedQuery query = em.createNamedQuery("findUserSessions", PersistentUserSessionEntity.class); + query.setParameter("offline", offline); + + if (firstResult != -1) { + query.setFirstResult(firstResult); + } + if (maxResults != -1) { + query.setMaxResults(maxResults); + } + + List results = query.getResultList(); + List result = new ArrayList<>(); + List userSessionIds = new ArrayList<>(); + for (PersistentUserSessionEntity entity : results) { + result.add(toAdapter(entity)); + userSessionIds.add(entity.getUserSessionId()); + } + + TypedQuery query2 = em.createNamedQuery("findClientSessionsByUserSessions", PersistentClientSessionEntity.class); + query2.setParameter("userSessionIds", userSessionIds); + query2.setParameter("offline", offline); + List clientSessions = query2.getResultList(); + + // Assume both userSessions and clientSessions ordered by userSessionId + int j=0; + for (UserSessionModel ss : result) { + PersistentUserSessionAdapter userSession = (PersistentUserSessionAdapter) ss; + List currentClientSessions = userSession.getClientSessions(); // This is empty now and we want to fill it + + boolean next = true; + while (next && j clientSessions = new LinkedList<>(); + return new PersistentUserSessionAdapter(model, realm, user, clientSessions); + } + + private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) { + ClientModel client = realm.getClientById(entity.getClientId()); + + PersistentClientSessionModel model = new PersistentClientSessionModel(); + model.setClientSessionId(entity.getClientSessionId()); + model.setClientId(entity.getClientId()); + model.setUserSessionId(userSession.getId()); + model.setUserId(userSession.getUser().getId()); + model.setData(entity.getData()); + return new PersistentClientSessionAdapter(model, realm, client, userSession); + } + + @Override + public int getUserSessionsCount(boolean offline) { + Query query = em.createNamedQuery("findUserSessionsCount"); + query.setParameter("offline", offline); + Number n = (Number) query.getSingleResult(); + return n.intValue(); + } + + @Override + public void close() { + + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java new file mode 100644 index 0000000000..87a42cceab --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java @@ -0,0 +1,44 @@ +package org.keycloak.models.jpa.session; + +import javax.persistence.EntityManager; + +import org.keycloak.Config; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.session.UserSessionPersisterProviderFactory; + +/** + * @author Marek Posolda + */ +public class JpaUserSessionPersisterProviderFactory implements UserSessionPersisterProviderFactory { + + public static final String ID = "jpa"; + + @Override + public UserSessionPersisterProvider create(KeycloakSession session) { + EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); + return new JpaUserSessionPersisterProvider(session, em); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return ID; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java new file mode 100644 index 0000000000..a11b87516a --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -0,0 +1,132 @@ +package org.keycloak.models.jpa.session; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +/** + * @author Marek Posolda + */ +@NamedQueries({ + @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (select u from PersistentUserSessionEntity u where u.realmId=:realmId)"), + @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId=:clientId"), + @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (select u from PersistentUserSessionEntity u where u.userId=:userId)"), + @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"), + @NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId NOT IN (select u.userSessionId from PersistentUserSessionEntity u)"), + @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"), + @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"), +}) +@Table(name="OFFLINE_CLIENT_SESSION") +@Entity +@IdClass(PersistentClientSessionEntity.Key.class) +public class PersistentClientSessionEntity { + + @Id + @Column(name="CLIENT_SESSION_ID", length = 36) + protected String clientSessionId; + + @Column(name = "USER_SESSION_ID", length = 36) + protected String userSessionId; + + @Column(name="CLIENT_ID", length = 36) + protected String clientId; + + @Id + @Column(name = "OFFLINE") + protected boolean offline; + + @Column(name="DATA") + protected String data; + + public String getClientSessionId() { + return clientSessionId; + } + + public void setClientSessionId(String clientSessionId) { + this.clientSessionId = clientSessionId; + } + + public String getUserSessionId() { + return userSessionId; + } + + public void setUserSessionId(String userSessionId) { + this.userSessionId = userSessionId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public boolean isOffline() { + return offline; + } + + public void setOffline(boolean offline) { + this.offline = offline; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public static class Key implements Serializable { + + protected String clientSessionId; + + protected boolean offline; + + public Key() { + } + + public Key(String clientSessionId, boolean offline) { + this.clientSessionId = clientSessionId; + this.offline = offline; + } + + public String getClientSessionId() { + return clientSessionId; + } + + public boolean isOffline() { + return offline; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (this.clientSessionId != null ? !this.clientSessionId.equals(key.clientSessionId) : key.clientSessionId != null) return false; + if (offline != key.offline) return false; + + return true; + } + + @Override + public int hashCode() { + int result = this.clientSessionId != null ? this.clientSessionId.hashCode() : 0; + result = 31 * result + (offline ? 1 : 0); + return result; + } + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java new file mode 100644 index 0000000000..f739091fe9 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java @@ -0,0 +1,147 @@ +package org.keycloak.models.jpa.session; + +import java.io.Serializable; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.keycloak.models.jpa.entities.UserEntity; + +/** + * @author Marek Posolda + */ +@NamedQueries({ + @NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId=:realmId"), + @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"), + @NamedQuery(name="deleteDetachedUserSessions", query="delete from PersistentUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from PersistentClientSessionEntity c)"), + @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where offline=:offline"), + @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId") + +}) +@Table(name="OFFLINE_USER_SESSION") +@Entity +@IdClass(PersistentUserSessionEntity.Key.class) +public class PersistentUserSessionEntity { + + @Id + @Column(name="USER_SESSION_ID", length = 36) + protected String userSessionId; + + @Column(name = "REALM_ID", length = 36) + protected String realmId; + + @Column(name="USER_ID", length = 36) + protected String userId; + + @Column(name = "LAST_SESSION_REFRESH") + protected int lastSessionRefresh; + + @Id + @Column(name = "OFFLINE") + protected boolean offline; + + @Column(name="DATA") + protected String data; + + public String getUserSessionId() { + return userSessionId; + } + + public void setUserSessionId(String userSessionId) { + this.userSessionId = userSessionId; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + public void setLastSessionRefresh(int lastSessionRefresh) { + this.lastSessionRefresh = lastSessionRefresh; + } + + public boolean isOffline() { + return offline; + } + + public void setOffline(boolean offline) { + this.offline = offline; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public static class Key implements Serializable { + + protected String userSessionId; + + protected boolean offline; + + public Key() { + } + + public Key(String userSessionId, boolean offline) { + this.userSessionId = userSessionId; + this.offline = offline; + } + + public String getUserSessionId() { + return userSessionId; + } + + public boolean isOffline() { + return offline; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false; + if (offline != key.offline) return false; + + return true; + } + + @Override + public int hashCode() { + int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0; + result = 31 * result + (offline ? 1 : 0); + return result; + } + } +} diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory new file mode 100644 index 0000000000..b478dda7fa --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory @@ -0,0 +1 @@ +org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory \ No newline at end of file diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 214733af8d..358e6f2d28 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -10,10 +10,6 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.ModelException; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -23,17 +19,13 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.entities.FederatedIdentityEntity; -import org.keycloak.models.entities.OfflineClientSessionEntity; -import org.keycloak.models.entities.OfflineUserSessionEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.utils.CredentialValidation; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -407,36 +399,6 @@ public class MongoUserProvider implements UserProvider { .and("clientId").is(client.getId()) .get(); getMongoStore().removeEntities(MongoUserConsentEntity.class, query, false, invocationContext); - - // Remove all offlineClientSessions - query = new QueryBuilder() - .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId()) - .get(); - List users = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext); - for (MongoUserEntity user : users) { - boolean anyRemoved = false; - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clientSession : userSession.getOfflineClientSessions()) { - if (clientSession.getClientId().equals(client.getId())) { - userSession.getOfflineClientSessions().remove(clientSession); - anyRemoved = true; - break; - } - } - - // Check if it was last clientSession. Then remove userSession too - if (userSession.getOfflineClientSessions().size() == 0) { - user.getOfflineUserSessions().remove(userSession); - anyRemoved = true; - break; - } - } - - if (anyRemoved) { - getMongoStore().updateEntity(user, invocationContext); - } - - } } @Override @@ -482,205 +444,4 @@ public class MongoUserProvider implements UserProvider { // Not supported yet return null; } - - @Override - public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - - if (user.getOfflineUserSessions() == null) { - user.setOfflineUserSessions(new ArrayList()); - } - - if (getUserSessionEntityById(user, userSession.getUserSessionId()) != null) { - throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername()); - } - - OfflineUserSessionEntity entity = new OfflineUserSessionEntity(); - entity.setUserSessionId(userSession.getUserSessionId()); - entity.setData(userSession.getData()); - entity.setOfflineClientSessions(new ArrayList()); - user.getOfflineUserSessions().add(entity); - - getMongoStore().updateEntity(user, invocationContext); - } - - private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) { - OfflineUserSessionModel model = new OfflineUserSessionModel(); - model.setUserSessionId(entity.getUserSessionId()); - model.setData(entity.getData()); - return model; - } - - private OfflineUserSessionEntity getUserSessionEntityById(MongoUserEntity user, String userSessionId) { - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) { - if (entity.getUserSessionId().equals(userSessionId)) { - return entity; - } - } - } - return null; - } - - - @Override - public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - - OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId); - return entity==null ? null : toModel(entity); - } - - @Override - public Collection getOfflineUserSessions(RealmModel realm, UserModel userModel) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - - if (user.getOfflineUserSessions()==null) { - return Collections.emptyList(); - } else { - List result = new ArrayList<>(); - for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) { - result.add(toModel(entity)); - } - return result; - } - } - - @Override - public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - - OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId); - if (entity != null) { - user.getOfflineUserSessions().remove(entity); - getMongoStore().updateEntity(user, invocationContext); - return true; - } else { - return false; - } - } - - @Override - public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) { - MongoUserEntity user = getUserById(clientSession.getUserId(), realm).getUser(); - - OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId()); - if (userSessionEntity == null) { - throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername()); - } - - OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity(); - clEntity.setClientSessionId(clientSession.getClientSessionId()); - clEntity.setClientId(clientSession.getClientId()); - clEntity.setData(clientSession.getData()); - - userSessionEntity.getOfflineClientSessions().add(clEntity); - getMongoStore().updateEntity(user, invocationContext); - } - - @Override - public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - if (clSession.getClientSessionId().equals(clientSessionId)) { - return toModel(clSession, user.getId(), userSession.getUserSessionId()); - } - } - } - } - - return null; - } - - private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userId, String userSessionId) { - OfflineClientSessionModel model = new OfflineClientSessionModel(); - model.setClientSessionId(cls.getClientSessionId()); - model.setClientId(cls.getClientId()); - model.setUserId(userId); - model.setData(cls.getData()); - model.setUserSessionId(userSessionId); - return model; - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, UserModel userModel) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - - List result = new ArrayList<>(); - - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - result.add(toModel(clSession, user.getId(), userSession.getUserSessionId())); - } - } - } - - return result; - } - - @Override - public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) { - MongoUserEntity user = getUserById(userModel.getId(), realm).getUser(); - boolean updated = false; - - if (user.getOfflineUserSessions() != null) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - if (clSession.getClientSessionId().equals(clientSessionId)) { - userSession.getOfflineClientSessions().remove(clSession); - updated = true; - break; - } - } - - if (updated && userSession.getOfflineClientSessions().isEmpty()) { - user.getOfflineUserSessions().remove(userSession); - } - - if (updated) { - getMongoStore().updateEntity(user, invocationContext); - return true; - } - } - } - - return false; - } - - @Override - public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId()) - .get(); - return getMongoStore().countEntities(MongoUserEntity.class, query, invocationContext); - } - - @Override - public Collection getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId()) - .get(); - DBObject sort = new BasicDBObject("username", 1); - List users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext); - - List result = new LinkedList<>(); - for (MongoUserEntity user : users) { - for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) { - for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) { - if (clSession.getClientId().equals(client.getId())) { - OfflineClientSessionModel model = toModel(clSession, user.getId(), userSession.getUserSessionId()); - result.add(model); - } - } - } - } - - return result; - } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java new file mode 100644 index 0000000000..53917c2b11 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java @@ -0,0 +1,276 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.connections.mongo.api.MongoStore; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.entities.PersistentClientSessionEntity; +import org.keycloak.models.entities.PersistentUserSessionEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoOfflineUserSessionEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoOnlineUserSessionEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity; +import org.keycloak.models.session.PersistentClientSessionAdapter; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionAdapter; +import org.keycloak.models.session.PersistentUserSessionModel; +import org.keycloak.models.session.UserSessionPersisterProvider; + +/** + * @author Marek Posolda + */ +public class MongoUserSessionPersisterProvider implements UserSessionPersisterProvider { + + private final MongoStoreInvocationContext invocationContext; + private final KeycloakSession session; + + public MongoUserSessionPersisterProvider(KeycloakSession session, MongoStoreInvocationContext invocationContext) { + this.session = session; + this.invocationContext = invocationContext; + } + + protected MongoStore getMongoStore() { + return invocationContext.getMongoStore(); + } + + private Class getClazz(boolean offline) { + return offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; + } + + private MongoUserSessionEntity loadUserSession(String userSessionId, boolean offline) { + Class clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; + return getMongoStore().loadEntity(clazz, userSessionId, invocationContext); + } + + @Override + public void createUserSession(UserSessionModel userSession, boolean offline) { + PersistentUserSessionAdapter adapter = new PersistentUserSessionAdapter(userSession); + PersistentUserSessionModel model = adapter.getUpdatedModel(); + + MongoUserSessionEntity entity = offline ? new MongoOfflineUserSessionEntity() : new MongoOnlineUserSessionEntity(); + entity.setId(model.getUserSessionId()); + entity.setRealmId(adapter.getRealm().getId()); + entity.setUserId(adapter.getUser().getId()); + entity.setLastSessionRefresh(model.getLastSessionRefresh()); + entity.setData(model.getData()); + entity.setClientSessions(new ArrayList()); + getMongoStore().insertEntity(entity, invocationContext); + } + + @Override + public void createClientSession(ClientSessionModel clientSession, boolean offline) { + PersistentClientSessionAdapter adapter = new PersistentClientSessionAdapter(clientSession); + PersistentClientSessionModel model = adapter.getUpdatedModel(); + + MongoUserSessionEntity userSession = loadUserSession(model.getUserSessionId(), offline); + if (userSession == null) { + throw new ModelException("Not userSession found with ID " + clientSession.getUserSession().getId() + ". Requested by clientSession: " + clientSession.getId()); + } else { + PersistentClientSessionEntity entity = new PersistentClientSessionEntity(); + entity.setClientSessionId(clientSession.getId()); + entity.setClientId(clientSession.getClient().getId()); + entity.setData(model.getData()); + userSession.getClientSessions().add(entity); + getMongoStore().updateEntity(userSession, invocationContext); + } + } + + @Override + public void updateUserSession(UserSessionModel userSession, boolean offline) { + PersistentUserSessionAdapter adapter; + if (userSession instanceof PersistentUserSessionAdapter) { + adapter = (PersistentUserSessionAdapter) userSession; + } else { + adapter = new PersistentUserSessionAdapter(userSession); + } + + PersistentUserSessionModel model = adapter.getUpdatedModel(); + + MongoUserSessionEntity entity = loadUserSession(model.getUserSessionId(), offline); + if (entity == null) { + throw new ModelException("UserSession with ID " + userSession.getId() + ", offline: " + offline + " not found"); + } + entity.setLastSessionRefresh(model.getLastSessionRefresh()); + entity.setData(model.getData()); + + getMongoStore().updateEntity(entity, invocationContext); + } + + @Override + public void removeUserSession(String userSessionId, boolean offline) { + MongoUserSessionEntity entity = loadUserSession(userSessionId, offline); + if (entity != null) { + getMongoStore().removeEntity(entity, invocationContext); + } + } + + @Override + public void removeClientSession(String clientSessionId, boolean offline) { + DBObject query = new QueryBuilder() + .and("clientSessions.clientSessionId").is(clientSessionId) + .get(); + Class clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; + MongoUserSessionEntity userSession = getMongoStore().loadSingleEntity(clazz, query, invocationContext); + if (userSession != null) { + + PersistentClientSessionEntity found = null; + for (PersistentClientSessionEntity clientSession : userSession.getClientSessions()) { + if (clientSession.getClientSessionId().equals(clientSessionId)) { + found = clientSession; + break; + } + } + + if (found != null) { + userSession.getClientSessions().remove(found); + + // Remove userSession if it was last clientSession attached + if (userSession.getClientSessions().size() == 0) { + getMongoStore().removeEntity(userSession, invocationContext); + } else { + getMongoStore().updateEntity(userSession, invocationContext); + } + } + } + } + + @Override + public void onRealmRemoved(RealmModel realm) { + DBObject query = new QueryBuilder() + .and("realmId").is(realm.getId()) + .get(); + getMongoStore().removeEntities(MongoOnlineUserSessionEntity.class, query, false, invocationContext); + getMongoStore().removeEntities(MongoOfflineUserSessionEntity.class, query, false, invocationContext); + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + DBObject query = new QueryBuilder() + .and("clientSessions.clientId").is(client.getId()) + .get(); + + List userSessions = getMongoStore().loadEntities(MongoOnlineUserSessionEntity.class, query, invocationContext); + for (MongoOnlineUserSessionEntity userSession : userSessions) { + removeClientSessionOfClient(userSession, client.getId()); + } + + List userSessions2 = getMongoStore().loadEntities(MongoOfflineUserSessionEntity.class, query, invocationContext); + for (MongoOfflineUserSessionEntity userSession : userSessions2) { + removeClientSessionOfClient(userSession, client.getId()); + } + } + + private void removeClientSessionOfClient(MongoUserSessionEntity userSession, String clientId) { + PersistentClientSessionEntity found = null; + for (PersistentClientSessionEntity clientSession : userSession.getClientSessions()) { + if (clientSession.getClientId().equals(clientId)) { + found = clientSession; + break; + } + } + + if (found != null) { + userSession.getClientSessions().remove(found); + + // Remove userSession if it was last clientSession attached + if (userSession.getClientSessions().size() == 0) { + getMongoStore().removeEntity(userSession, invocationContext); + } else { + getMongoStore().updateEntity(userSession, invocationContext); + } + } + } + + @Override + public void onUserRemoved(RealmModel realm, UserModel user) { + DBObject query = new QueryBuilder() + .and("userId").is(user.getId()) + .get(); + getMongoStore().removeEntities(MongoOnlineUserSessionEntity.class, query, false, invocationContext); + getMongoStore().removeEntities(MongoOfflineUserSessionEntity.class, query, false, invocationContext); + } + + @Override + public void clearDetachedUserSessions() { + DBObject query = new QueryBuilder() + .and("clientSessions").is(Collections.emptyList()) + .get(); + getMongoStore().removeEntities(MongoOnlineUserSessionEntity.class, query, false, invocationContext); + getMongoStore().removeEntities(MongoOfflineUserSessionEntity.class, query, false, invocationContext); + } + + @Override + public int getUserSessionsCount(boolean offline) { + DBObject query = new QueryBuilder() + .get(); + + Class clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; + return getMongoStore().countEntities(clazz, query, invocationContext); + } + + @Override + public List loadUserSessions(int firstResult, int maxResults, boolean offline) { + DBObject query = new QueryBuilder() + .get(); + DBObject sort = new BasicDBObject("id", 1); + + Class clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; + + List entities = getMongoStore().loadEntities(clazz, query, sort, firstResult, maxResults, invocationContext); + + List results = new LinkedList<>(); + for (MongoUserSessionEntity entity : entities) { + PersistentUserSessionAdapter userSession = toAdapter(entity, offline); + results.add(userSession); + } + return results; + } + + private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity, boolean offline) { + RealmModel realm = session.realms().getRealm(entity.getRealmId()); + UserModel user = session.users().getUserById(entity.getUserId(), realm); + + PersistentUserSessionModel model = new PersistentUserSessionModel(); + model.setUserSessionId(entity.getId()); + model.setLastSessionRefresh(entity.getLastSessionRefresh()); + model.setData(entity.getData()); + + List clientSessions = new LinkedList<>(); + PersistentUserSessionAdapter userSessionAdapter = new PersistentUserSessionAdapter(model, realm, user, clientSessions); + for (PersistentClientSessionEntity clientSessEntity : entity.getClientSessions()) { + PersistentClientSessionAdapter clientSessAdapter = toAdapter(realm, userSessionAdapter, offline, clientSessEntity); + clientSessions.add(clientSessAdapter); + } + + return userSessionAdapter; + } + + private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, boolean offline, PersistentClientSessionEntity entity) { + ClientModel client = realm.getClientById(entity.getClientId()); + + PersistentClientSessionModel model = new PersistentClientSessionModel(); + model.setClientSessionId(entity.getClientSessionId()); + model.setClientId(entity.getClientId()); + model.setUserSessionId(userSession.getId()); + model.setUserId(userSession.getUser().getId()); + model.setData(entity.getData()); + return new PersistentClientSessionAdapter(model, realm, client, userSession); + } + + @Override + public void close() { + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java new file mode 100644 index 0000000000..c2950fbc66 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java @@ -0,0 +1,42 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import org.keycloak.Config; +import org.keycloak.connections.mongo.MongoConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.session.UserSessionPersisterProviderFactory; + +/** + * @author Marek Posolda + */ +public class MongoUserSessionPersisterProviderFactory implements UserSessionPersisterProviderFactory { + + public static final String ID = "mongo"; + + @Override + public UserSessionPersisterProvider create(KeycloakSession session) { + MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class); + return new MongoUserSessionPersisterProvider(session, connection.getInvocationContext()); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return ID; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 9f13e632dd..16c4431c20 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -8,8 +8,6 @@ import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; import org.keycloak.models.OTPPolicy; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.KeycloakSession; @@ -22,19 +20,15 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.entities.CredentialEntity; -import org.keycloak.models.entities.OfflineClientSessionEntity; -import org.keycloak.models.entities.OfflineUserSessionEntity; import org.keycloak.models.entities.UserConsentEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.utils.MongoModelUtils; -import org.keycloak.models.utils.HmacOTP; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.util.Time; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java new file mode 100644 index 0000000000..dd59869e47 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java @@ -0,0 +1,10 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.connections.mongo.api.MongoCollection; + +/** + * @author Marek Posolda + */ +@MongoCollection(collectionName = "offlineUserSessions") +public class MongoOfflineUserSessionEntity extends MongoUserSessionEntity { +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java new file mode 100644 index 0000000000..7df63a97e3 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java @@ -0,0 +1,10 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.connections.mongo.api.MongoCollection; + +/** + * @author Marek Posolda + */ +@MongoCollection(collectionName = "userSessions") +public class MongoOnlineUserSessionEntity extends MongoUserSessionEntity { +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java new file mode 100644 index 0000000000..1da6d310dd --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java @@ -0,0 +1,15 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.PersistentUserSessionEntity; + +/** + * @author Marek Posolda + */ +public abstract class MongoUserSessionEntity extends PersistentUserSessionEntity implements MongoIdentifiableEntity { + + @Override + public void afterRemove(MongoStoreInvocationContext invocationContext) { + } +} diff --git a/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory b/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory new file mode 100644 index 0000000000..b8acb44672 --- /dev/null +++ b/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory @@ -0,0 +1 @@ +org.keycloak.models.mongo.keycloak.adapters.MongoUserSessionPersisterProviderFactory \ No newline at end of file diff --git a/model/sessions-infinispan/pom.xml b/model/sessions-infinispan/pom.xml index ee64a6be0f..a9cdf35bfe 100755 --- a/model/sessions-infinispan/pom.xml +++ b/model/sessions-infinispan/pom.xml @@ -30,5 +30,10 @@ org.infinispan infinispan-core + + junit + junit + test + diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java index 179a1f0211..a0b653199c 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java @@ -26,13 +26,16 @@ public class ClientSessionAdapter implements ClientSessionModel { private Cache cache; private RealmModel realm; private ClientSessionEntity entity; + private boolean offline; - public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, ClientSessionEntity entity) { + public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, + ClientSessionEntity entity, boolean offline) { this.session = session; this.provider = provider; this.cache = cache; this.realm = realm; this.entity = entity; + this.offline = offline; } @Override @@ -51,8 +54,8 @@ public class ClientSessionAdapter implements ClientSessionModel { } @Override - public UserSessionModel getUserSession() { - return entity.getUserSession() != null ? provider.getUserSession(realm, entity.getUserSession()) : null; + public UserSessionAdapter getUserSession() { + return entity.getUserSession() != null ? provider.getUserSession(realm, entity.getUserSession(), offline) : null; } @Override @@ -63,14 +66,15 @@ public class ClientSessionAdapter implements ClientSessionModel { } entity.setUserSession(null); } else { + UserSessionAdapter userSessionAdapter = (UserSessionAdapter) userSession; if (entity.getUserSession() != null) { if (entity.getUserSession().equals(userSession.getId())) { return; } else { - provider.dettachSession(userSession, this); + provider.dettachSession(userSessionAdapter, this); } } else { - provider.attachSession(userSession, this); + provider.attachSession(userSessionAdapter, this); } entity.setUserSession(userSession.getId()); @@ -113,7 +117,8 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public Set getRoles() { - return entity.getRoles(); + if (entity.getRoles() == null || entity.getRoles().isEmpty()) return Collections.emptySet(); + return new HashSet<>(entity.getRoles()); } @Override @@ -124,7 +129,8 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public Set getProtocolMappers() { - return entity.getProtocolMappers(); + if (entity.getProtocolMappers() == null || entity.getProtocolMappers().isEmpty()) return Collections.emptySet(); + return new HashSet<>(entity.getProtocolMappers()); } @Override diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 83776df17a..dbfecb4357 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -7,6 +7,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; +import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -18,6 +19,7 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper; +import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionsOfUserSessionMapper; import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer; import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer; import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper; @@ -36,6 +38,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Stian Thorgersen @@ -46,18 +49,25 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { private final KeycloakSession session; private final Cache sessionCache; + private final Cache offlineSessionCache; private final Cache loginFailureCache; private final InfinispanKeycloakTransaction tx; - public InfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache loginFailureCache) { + public InfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache offlineSessionCache, + Cache loginFailureCache) { this.session = session; this.sessionCache = sessionCache; + this.offlineSessionCache = offlineSessionCache; this.loginFailureCache = loginFailureCache; this.tx = new InfinispanKeycloakTransaction(); session.getTransaction().enlistAfterCompletion(tx); } + protected Cache getCache(boolean offline) { + return offline ? offlineSessionCache : sessionCache; + } + @Override public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) { String id = KeycloakModelUtils.generateId(); @@ -70,7 +80,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { tx.put(sessionCache, id, entity); - return wrap(realm, entity); + return wrap(realm, entity, false); } @Override @@ -95,29 +105,57 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { tx.put(sessionCache, id, entity); - return wrap(realm, entity); + return wrap(realm, entity, false); } @Override public ClientSessionModel getClientSession(RealmModel realm, String id) { - ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id); - return wrap(realm, entity); + return getClientSession(realm, id, false); + } + + protected ClientSessionModel getClientSession(RealmModel realm, String id, boolean offline) { + Cache cache = getCache(offline); + ClientSessionEntity entity = (ClientSessionEntity) cache.get(id); + + // Chance created in this transaction + if (entity == null) { + entity = (ClientSessionEntity) tx.get(cache, id); + } + + return wrap(realm, entity, offline); } @Override public ClientSessionModel getClientSession(String id) { ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id); + + // Chance created in this transaction + if (entity == null) { + entity = (ClientSessionEntity) tx.get(sessionCache, id); + } + if (entity != null) { RealmModel realm = session.realms().getRealm(entity.getRealm()); - return wrap(realm, entity); + return wrap(realm, entity, false); } return null; } @Override public UserSessionModel getUserSession(RealmModel realm, String id) { - UserSessionEntity entity = (UserSessionEntity) sessionCache.get(id); - return wrap(realm, entity); + return getUserSession(realm, id, false); + } + + protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { + Cache cache = getCache(offline); + UserSessionEntity entity = (UserSessionEntity) cache.get(id); + + // Chance created in this transaction + if (entity == null) { + entity = (UserSessionEntity) tx.get(cache, id); + } + + return wrap(realm, entity, offline); } @Override @@ -127,7 +165,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { .reducedWith(new FirstResultReducer()) .execute(); - return wrapUserSessions(realm, sessions.values()); + return wrapUserSessions(realm, sessions.values(), false); } @Override @@ -137,7 +175,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { .reducedWith(new FirstResultReducer()) .execute(); - return wrapUserSessions(realm, sessions.values()); + return wrapUserSessions(realm, sessions.values(), false); } @Override @@ -147,7 +185,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { .reducedWith(new FirstResultReducer()) .execute(); - List userSessionModels = wrapUserSessions(realm, sessions.values()); + List userSessionModels = wrapUserSessions(realm, sessions.values(), false); if (userSessionModels.isEmpty()) return null; return userSessionModels.get(0); } @@ -159,7 +197,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - Map map = new MapReduceTask(sessionCache) + return getUserSessions(realm, client, firstResult, maxResults, false); + } + + protected List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults, boolean offline) { + Cache cache = getCache(offline); + + Map map = new MapReduceTask(cache) .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) .reducedWith(new LargestResultReducer()) .execute(); @@ -192,9 +236,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { List userSessions = new LinkedList(); for (Map.Entry e : sessionTimestamps) { - UserSessionEntity userSessionEntity = (UserSessionEntity) sessionCache.get(e.getKey()); + UserSessionEntity userSessionEntity = (UserSessionEntity) cache.get(e.getKey()); if (userSessionEntity != null) { - userSessions.add(wrap(realm, userSessionEntity)); + userSessions.add(wrap(realm, userSessionEntity, offline)); } } @@ -214,13 +258,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { .reducedWith(new FirstResultReducer()) .execute(); - return wrapUserSessions(realm, sessions.values()); + return wrapUserSessions(realm, sessions.values(), false); } @Override public int getActiveUserSessions(RealmModel realm, ClientModel client) { - Map map = new MapReduceTask(sessionCache) + return getUserSessionsCount(realm, client, false); + } + + protected int getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { + Cache cache = getCache(offline); + + Map map = new MapReduceTask(cache) .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) .reducedWith(new LargestResultReducer()).execute(); @@ -234,13 +284,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void removeUserSessions(RealmModel realm, UserModel user) { - Map sessions = new MapReduceTask(sessionCache) + removeUserSessions(realm, user, false); + } + + protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { + Cache cache = getCache(offline); + + Map sessions = new MapReduceTask(cache) .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()).emitKey()) .reducedWith(new FirstResultReducer()) .execute(); for (String id : sessions.keySet()) { - removeUserSession(realm, id); + removeUserSession(realm, id, offline); } } @@ -271,13 +327,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void removeUserSessions(RealmModel realm) { - Map ids = new MapReduceTask(sessionCache) + removeUserSessions(realm, false); + } + + protected void removeUserSessions(RealmModel realm, boolean offline) { + Cache cache = getCache(offline); + + Map ids = new MapReduceTask(cache) .mappedWith(SessionMapper.create(realm.getId()).emitKey()) .reducedWith(new FirstResultReducer()) .execute(); for (String id : ids.keySet()) { - sessionCache.remove(id); + cache.remove(id); } } @@ -319,25 +381,39 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void onRealmRemoved(RealmModel realm) { - removeUserSessions(realm); + removeUserSessions(realm, true); + removeUserSessions(realm, false); removeAllUserLoginFailures(realm); } @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - Map map = new MapReduceTask(sessionCache) - .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitKey()) + onClientRemoved(realm, client, true); + onClientRemoved(realm, client, false); + } + + private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) { + Cache cache = getCache(offline); + + Map map = new MapReduceTask(cache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId())) .reducedWith(new FirstResultReducer()) .execute(); - for (String id : map.keySet()) { - tx.remove(sessionCache, id); + for (Map.Entry entry : map.entrySet()) { + + // detach from userSession + ClientSessionAdapter adapter = wrap(realm, entry.getValue(), offline); + adapter.setUserSession(null); + + tx.remove(cache, entry.getKey()); } } @Override public void onUserRemoved(RealmModel realm, UserModel user) { - removeUserSessions(realm, user); + removeUserSessions(realm, user, true); + removeUserSessions(realm, user, false); loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getUsername())); loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail())); @@ -347,20 +423,26 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void close() { } - void attachSession(UserSessionModel userSession, ClientSessionModel clientSession) { - UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); + void attachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) { + UserSessionEntity entity = userSession.getEntity(); String clientSessionId = clientSession.getId(); if (entity.getClientSessions() == null) { entity.setClientSessions(new HashSet()); } if (!entity.getClientSessions().contains(clientSessionId)) { entity.getClientSessions().add(clientSessionId); - tx.replace(sessionCache, entity.getId(), entity); + userSession.update(); } } @Override public void removeClientSession(RealmModel realm, ClientSessionModel clientSession) { + removeClientSession(realm, clientSession, false); + } + + protected void removeClientSession(RealmModel realm, ClientSessionModel clientSession, boolean offline) { + Cache cache = getCache(offline); + UserSessionModel userSession = clientSession.getUserSession(); if (userSession != null) { UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); @@ -368,34 +450,40 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.getClientSessions().remove(clientSession.getId()); } - tx.replace(sessionCache, entity.getId(), entity); + tx.replace(cache, entity.getId(), entity); } - tx.remove(sessionCache, clientSession.getId()); + tx.remove(cache, clientSession.getId()); } - void dettachSession(UserSessionModel userSession, ClientSessionModel clientSession) { - UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); + void dettachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) { + UserSessionEntity entity = userSession.getEntity(); String clientSessionId = clientSession.getId(); if (entity.getClientSessions() != null && entity.getClientSessions().contains(clientSessionId)) { entity.getClientSessions().remove(clientSessionId); if (entity.getClientSessions().isEmpty()) { entity.setClientSessions(null); } - tx.replace(sessionCache, entity.getId(), entity); + userSession.update(); } } protected void removeUserSession(RealmModel realm, String userSessionId) { - tx.remove(sessionCache, userSessionId); + removeUserSession(realm, userSessionId, false); + } - Map map = new MapReduceTask(sessionCache) + protected void removeUserSession(RealmModel realm, String userSessionId, boolean offline) { + Cache cache = getCache(offline); + + tx.remove(cache, userSessionId); + + Map map = new MapReduceTask(cache) .mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey()) .reducedWith(new FirstResultReducer()) .execute(); for (String id : map.keySet()) { - tx.remove(sessionCache, id); + tx.remove(cache, id); } } @@ -404,20 +492,22 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return tx; } - UserSessionModel wrap(RealmModel realm, UserSessionEntity entity) { - return entity != null ? new UserSessionAdapter(session, this, sessionCache, realm, entity) : null; + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { + Cache cache = getCache(offline); + return entity != null ? new UserSessionAdapter(session, this, cache, realm, entity, offline) : null; } - List wrapUserSessions(RealmModel realm, Collection entities) { + List wrapUserSessions(RealmModel realm, Collection entities, boolean offline) { List models = new LinkedList(); for (UserSessionEntity e : entities) { - models.add(wrap(realm, e)); + models.add(wrap(realm, e, offline)); } return models; } - ClientSessionModel wrap(RealmModel realm, ClientSessionEntity entity) { - return entity != null ? new ClientSessionAdapter(session, this, sessionCache, realm, entity) : null; + ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) { + Cache cache = getCache(offline); + return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null; } @@ -425,14 +515,113 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null; } - List wrapClientSessions(RealmModel realm, Collection entities) { + List wrapClientSessions(RealmModel realm, Collection entities, boolean offline) { List models = new LinkedList(); for (ClientSessionEntity e : entities) { - models.add(wrap(realm, e)); + models.add(wrap(realm, e, offline)); } return models; } + + @Override + public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { + UserSessionEntity entity = new UserSessionEntity(); + entity.setId(userSession.getId()); + entity.setRealm(userSession.getRealm().getId()); + + entity.setAuthMethod(userSession.getAuthMethod()); + entity.setBrokerSessionId(userSession.getBrokerSessionId()); + entity.setBrokerUserId(userSession.getBrokerUserId()); + entity.setIpAddress(userSession.getIpAddress()); + entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + entity.setLoginUsername(userSession.getLoginUsername()); + entity.setNotes(userSession.getNotes()); + entity.setRememberMe(userSession.isRememberMe()); + entity.setStarted(userSession.getStarted()); + entity.setState(userSession.getState()); + entity.setUser(userSession.getUser().getId()); + + tx.put(offlineSessionCache, userSession.getId(), entity); + return wrap(userSession.getRealm(), entity, true); + } + + @Override + public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) { + return getUserSession(realm, userSessionId, true); + } + + @Override + public void removeOfflineUserSession(RealmModel realm, String userSessionId) { + removeUserSession(realm, userSessionId, true); + } + + @Override + public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) { + ClientSessionEntity entity = new ClientSessionEntity(); + entity.setId(clientSession.getId()); + entity.setRealm(clientSession.getRealm().getId()); + + entity.setAction(clientSession.getAction()); + entity.setAuthenticatorStatus(clientSession.getExecutionStatus()); + entity.setAuthMethod(clientSession.getAuthMethod()); + if (clientSession.getAuthenticatedUser() != null) { + entity.setAuthUserId(clientSession.getAuthenticatedUser().getId()); + } + entity.setClient(clientSession.getClient().getId()); + entity.setNotes(clientSession.getNotes()); + entity.setProtocolMappers(clientSession.getProtocolMappers()); + entity.setRedirectUri(clientSession.getRedirectUri()); + entity.setRoles(clientSession.getRoles()); + entity.setTimestamp(clientSession.getTimestamp()); + entity.setUserSessionNotes(clientSession.getUserSessionNotes()); + + tx.put(offlineSessionCache, clientSession.getId(), entity); + return wrap(clientSession.getRealm(), entity, true); + } + + @Override + public ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId) { + return getClientSession(realm, clientSessionId, true); + } + + @Override + public List getOfflineClientSessions(RealmModel realm, UserModel user) { + Map sessions = new MapReduceTask(offlineSessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + + List clientSessions = new LinkedList<>(); + for (UserSessionEntity userSession : sessions.values()) { + Set currClientSessions = userSession.getClientSessions(); + for (String clientSessionId : currClientSessions) { + ClientSessionEntity cls = (ClientSessionEntity) offlineSessionCache.get(clientSessionId); + if (cls != null) { + clientSessions.add(cls); + } + } + } + + return wrapClientSessions(realm, clientSessions, true); + } + + @Override + public void removeOfflineClientSession(RealmModel realm, String clientSessionId) { + ClientSessionModel clientSession = getOfflineClientSession(realm, clientSessionId); + removeClientSession(realm, clientSession, true); + } + + @Override + public int getOfflineSessionsCount(RealmModel realm, ClientModel client) { + return getUserSessionsCount(realm, client, true); + } + + @Override + public List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) { + return getUserSessions(realm, client, first, max, true); + } + class InfinispanKeycloakTransaction implements KeycloakTransaction { private boolean active; @@ -478,17 +667,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void put(Cache cache, Object key, Object value) { log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD, key); - if (tasks.containsKey(key)) { + Object taskKey = getTaskKey(cache, key); + if (tasks.containsKey(taskKey)) { throw new IllegalStateException("Can't add session: task in progress for session"); } else { - tasks.put(key, new CacheTask(cache, CacheOperation.ADD, key, value)); + tasks.put(taskKey, new CacheTask(cache, CacheOperation.ADD, key, value)); } } public void replace(Cache cache, Object key, Object value) { log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key); - CacheTask current = tasks.get(key); + Object taskKey = getTaskKey(cache, key); + CacheTask current = tasks.get(taskKey); if (current != null) { switch (current.operation) { case ADD: @@ -499,14 +690,40 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return; } } else { - tasks.put(key, new CacheTask(cache, CacheOperation.REPLACE, key, value)); + tasks.put(taskKey, new CacheTask(cache, CacheOperation.REPLACE, key, value)); } } public void remove(Cache cache, Object key) { log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key); - tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null)); + Object taskKey = getTaskKey(cache, key); + tasks.put(taskKey, new CacheTask(cache, CacheOperation.REMOVE, key, null)); + } + + // This is for possibility to lookup for session by id, which was created in this transaction + public Object get(Cache cache, Object key) { + Object taskKey = getTaskKey(cache, key); + CacheTask current = tasks.get(taskKey); + if (current != null) { + switch (current.operation) { + case ADD: + case REPLACE: + return current.value; } + } + + return null; + } + + private Object getTaskKey(Cache cache, Object key) { + if (key instanceof String) { + return new StringBuilder(cache.getName()) + .append("::") + .append(key.toString()).toString(); + } else { + // loginFailure cache + return key; + } } public class CacheTask { diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index ec6025b2f8..c88e4901f8 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -7,12 +7,18 @@ import org.keycloak.Config; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.compat.MemUserSessionProviderFactory; +import org.keycloak.models.sessions.infinispan.compat.SimpleUserSessionInitializer; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer; +import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader; +import org.keycloak.models.utils.KeycloakModelUtils; /** * Uses Infinispan to store user sessions. On EAP 6.4 (Infinispan 5.2) map reduce is not supported for local caches as a work around @@ -24,28 +30,19 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class); + private Config.Scope config; private Boolean compatMode; private MemUserSessionProviderFactory compatProviderFactory; @Override public UserSessionProvider create(KeycloakSession session) { - if (compatMode == null) { - synchronized (this) { - if (compatMode == null) { - compatMode = isCompatMode(session); - if (compatMode) { - compatProviderFactory = new MemUserSessionProviderFactory(); - log.info("Infinispan version doesn't support map reduce for local cache. Falling back to deprecated mem user session provider."); - } - } - } - } if (!compatMode) { InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); Cache cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); Cache loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); - return new InfinispanUserSessionProvider(session, cache, loginFailures); + return new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures); } else { return compatProviderFactory.create(session); } @@ -53,11 +50,67 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider @Override public void init(Config.Scope config) { + this.config = config; } @Override - public void postInit(KeycloakSessionFactory factory) { + public void postInit(final KeycloakSessionFactory factory) { + KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() { + @Override + public void run(KeycloakSession session) { + compatMode = isCompatMode(session); + if (compatMode) { + compatProviderFactory = new MemUserSessionProviderFactory(); + } + + log.debug("Clearing detached sessions from persistent storage"); + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + if (persister == null) { + throw new RuntimeException("userSessionPersister not configured. Please see the migration docs and upgrade your configuration"); + } else { + persister.clearDetachedUserSessions(); + } + } + + }); + + // Max count of worker errors. Initialization will end with exception when this number is reached + int maxErrors = config.getInt("maxErrors", 50); + + // Count of sessions to be computed in each segment + int sessionsPerSegment = config.getInt("sessionsPerSegment", 100); + + // TODO: Possibility to run this asynchronously to not block start time + loadPersistentSessions(factory, maxErrors, sessionsPerSegment); + } + + + @Override + public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) { + log.debug("Start pre-loading userSessions and clientSessions from persistent storage"); + + if (compatMode) { + SimpleUserSessionInitializer initializer = new SimpleUserSessionInitializer(sessionFactory, new OfflineUserSessionLoader(), sessionsPerSegment); + initializer.loadPersistentSessions(); + + } else { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + + InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions"); + initializer.initCache(); + initializer.loadPersistentSessions(); + } + + }); + } + + log.debug("Pre-loading userSessions and clientSessions from persistent storage finished"); } @Override @@ -72,11 +125,18 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider return "infinispan"; } - private static boolean isCompatMode(KeycloakSession session) { + private boolean isCompatMode(KeycloakSession session) { + // For unit tests + if (this.config.getBoolean("enforceCompat", false)) { + log.info("Enforced compatibility mode for infinispan. Falling back to deprecated mem user session provider."); + return true; + } + if (Version.getVersionShort() < Version.getVersionShort("5.3.0.Final")) { InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); Cache cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); if (cache.getAdvancedCache().getRpcManager() == null) { + log.info("Infinispan version doesn't support map reduce for local cache. Falling back to deprecated mem user session provider."); return true; } } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index c7104fbfe2..1601acb798 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -31,18 +31,27 @@ public class UserSessionAdapter implements UserSessionModel { private final UserSessionEntity entity; - public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, UserSessionEntity entity) { + private final boolean offline; + + public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, + UserSessionEntity entity, boolean offline) { this.session = session; this.provider = provider; this.cache = cache; this.realm = realm; this.entity = entity; + this.offline = offline; } public String getId() { return entity.getId(); } + @Override + public RealmModel getRealm() { + return realm; + } + @Override public String getBrokerSessionId() { return entity.getBrokerSessionId(); @@ -129,14 +138,14 @@ public class UserSessionAdapter implements UserSessionModel { @Override public List getClientSessions() { if (entity.getClientSessions() != null) { - List clientSessions = new LinkedList(); + List clientSessions = new LinkedList<>(); for (String c : entity.getClientSessions()) { - ClientSessionEntity clientSession = (ClientSessionEntity) cache.get(c); + ClientSessionModel clientSession = provider.getClientSession(realm, c, offline); if (clientSession != null) { clientSessions.add(clientSession); } } - return provider.wrapClientSessions(realm, clientSessions); + return clientSessions; } else { return Collections.emptyList(); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java index 947c605663..0e5f2b9784 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java @@ -4,6 +4,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -38,13 +39,21 @@ public class MemUserSessionProvider implements UserSessionProvider { private final ConcurrentHashMap clientSessions; private final ConcurrentHashMap loginFailures; - public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap userSessions, ConcurrentHashMap userSessionsByBrokerSessionId, ConcurrentHashMap> userSessionsByBrokerUserId, ConcurrentHashMap clientSessions, ConcurrentHashMap loginFailures) { + private final ConcurrentHashMap offlineUserSessions; + private final ConcurrentHashMap offlineClientSessions; + + public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap userSessions, ConcurrentHashMap userSessionsByBrokerSessionId, + ConcurrentHashMap> userSessionsByBrokerUserId, ConcurrentHashMap clientSessions, + ConcurrentHashMap loginFailures, + ConcurrentHashMap offlineUserSessions, ConcurrentHashMap offlineClientSessions) { this.session = session; this.userSessions = userSessions; this.clientSessions = clientSessions; this.loginFailures = loginFailures; this.userSessionsByBrokerSessionId = userSessionsByBrokerSessionId; this.userSessionsByBrokerUserId = userSessionsByBrokerUserId; + this.offlineUserSessions = offlineUserSessions; + this.offlineClientSessions = offlineClientSessions; } @Override @@ -189,6 +198,12 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public List getUserSessions(RealmModel realm, ClientModel client) { + return getUserSessions(realm, client, false); + } + + protected List getUserSessions(RealmModel realm, ClientModel client, boolean offline) { + ConcurrentHashMap clientSessions = offline ? this.offlineClientSessions : this.clientSessions; + List userSessionEntities = new LinkedList(); for (ClientSessionEntity s : clientSessions.values()) { String realmId = realm.getId(); @@ -210,7 +225,11 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - List userSessions = getUserSessions(realm, client); + return getUserSessions(realm, client, firstResult, maxResults, false); + } + + protected List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults, boolean offline) { + List userSessions = getUserSessions(realm, client, offline); if (firstResult > userSessions.size()) { return Collections.emptyList(); } @@ -221,7 +240,7 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public int getActiveUserSessions(RealmModel realm, ClientModel client) { - return getUserSessions(realm, client).size(); + return getUserSessions(realm, client, false).size(); } @Override @@ -229,40 +248,51 @@ public class MemUserSessionProvider implements UserSessionProvider { UserSessionEntity entity = getUserSessionEntity(realm, session.getId()); if (entity != null) { userSessions.remove(entity.getId()); - remove(entity); + remove(entity, false); } } @Override public void removeUserSessions(RealmModel realm, UserModel user) { - Iterator itr = userSessions.values().iterator(); + removeUserSessions(realm, user, false); + } + + protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { + Iterator itr = offline ? offlineUserSessions.values().iterator() : userSessions.values().iterator(); + while (itr.hasNext()) { UserSessionEntity s = itr.next(); if (s.getRealm().equals(realm.getId()) && s.getUser().equals(user.getId())) { itr.remove(); - remove(s); + remove(s, offline); } } } - protected void remove(UserSessionEntity s) { - if (s.getBrokerSessionId() != null) { - userSessionsByBrokerSessionId.remove(s.getBrokerSessionId()); - } - if (s.getBrokerUserId() != null) { - Set set = userSessionsByBrokerUserId.get(s.getBrokerUserId()); - if (set != null) { - synchronized (set) { - set.remove(s.getId()); - // this is a race condition :( - // Since it will be very rare for a user to have concurrent sessions, I'm hoping we never hit this - if (set.isEmpty()) userSessionsByBrokerUserId.remove(s.getBrokerUserId()); + protected void remove(UserSessionEntity s, boolean offline) { + if (offline) { + for (ClientSessionEntity clientSession : s.getClientSessions()) { + offlineClientSessions.remove(clientSession.getId()); + } + } else { + if (s.getBrokerSessionId() != null) { + userSessionsByBrokerSessionId.remove(s.getBrokerSessionId()); + } + if (s.getBrokerUserId() != null) { + Set set = userSessionsByBrokerUserId.get(s.getBrokerUserId()); + if (set != null) { + synchronized (set) { + set.remove(s.getId()); + // this is a race condition :( + // Since it will be very rare for a user to have concurrent sessions, I'm hoping we never hit this + if (set.isEmpty()) userSessionsByBrokerUserId.remove(s.getBrokerUserId()); + } } } + for (ClientSessionEntity clientSession : s.getClientSessions()) { + clientSessions.remove(clientSession.getId()); + } } - for (ClientSessionEntity clientSession : s.getClientSessions()) { - clientSessions.remove(clientSession.getId()); - } } @Override @@ -273,7 +303,7 @@ public class MemUserSessionProvider implements UserSessionProvider { if (s.getRealm().equals(realm.getId()) && (s.getLastSessionRefresh() < Time.currentTime() - realm.getSsoSessionIdleTimeout() || s.getStarted() < Time.currentTime() - realm.getSsoSessionMaxLifespan())) { itr.remove(); - remove(s); + remove(s, false); } } int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); @@ -288,16 +318,19 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public void removeUserSessions(RealmModel realm) { - Iterator itr = userSessions.values().iterator(); + removeUserSessions(realm, false); + } + + protected void removeUserSessions(RealmModel realm, boolean offline) { + Iterator itr = offline ? offlineUserSessions.values().iterator() : userSessions.values().iterator(); while (itr.hasNext()) { UserSessionEntity s = itr.next(); if (s.getRealm().equals(realm.getId())) { itr.remove(); - - remove(s); + remove(s, offline); } } - Iterator citr = clientSessions.values().iterator(); + Iterator citr = offline ? offlineClientSessions.values().iterator() : clientSessions.values().iterator(); while (citr.hasNext()) { ClientSessionEntity c = citr.next(); if (c.getSession() == null && c.getRealmId().equals(realm.getId())) { @@ -340,15 +373,23 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public void onRealmRemoved(RealmModel realm) { - removeUserSessions(realm); + removeUserSessions(realm, true); + removeUserSessions(realm, false); removeAllUserLoginFailures(realm); } @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - for (ClientSessionEntity e : clientSessions.values()) { + onClientRemoved(realm, client, true); + onClientRemoved(realm, client, false); + } + + private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) { + ConcurrentHashMap clientSessionsMap = offline ? offlineClientSessions : clientSessions; + + for (ClientSessionEntity e : clientSessionsMap.values()) { if (e.getRealmId().equals(realm.getId()) && e.getClientId().equals(client.getId())) { - clientSessions.remove(e.getId()); + clientSessionsMap.remove(e.getId()); e.getSession().removeClientSession(e); } } @@ -356,12 +397,130 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public void onUserRemoved(RealmModel realm, UserModel user) { - removeUserSessions(realm, user); + removeUserSessions(realm, user, true); + removeUserSessions(realm, user, false); loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), user.getUsername())); loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), user.getEmail())); } + + @Override + public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { + UserSessionEntity entity = new UserSessionEntity(); + entity.setId(userSession.getId()); + entity.setRealm(userSession.getRealm().getId()); + + entity.setAuthMethod(userSession.getAuthMethod()); + entity.setBrokerSessionId(userSession.getBrokerSessionId()); + entity.setBrokerUserId(userSession.getBrokerUserId()); + entity.setIpAddress(userSession.getIpAddress()); + entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + entity.setLoginUsername(userSession.getLoginUsername()); + if (userSession.getNotes() != null) { + entity.getNotes().putAll(userSession.getNotes()); + } + entity.setRememberMe(userSession.isRememberMe()); + entity.setStarted(userSession.getStarted()); + entity.setState(userSession.getState()); + entity.setUser(userSession.getUser().getId()); + + offlineUserSessions.put(userSession.getId(), entity); + return new UserSessionAdapter(session, this, userSession.getRealm(), entity); + } + + @Override + public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) { + UserSessionEntity entity = offlineUserSessions.get(userSessionId); + if (entity != null && entity.getRealm().equals(realm.getId())) { + return new UserSessionAdapter(session, this, realm, entity); + } else { + return null; + } + } + + @Override + public void removeOfflineUserSession(RealmModel realm, String userSessionId) { + UserSessionEntity entity = offlineUserSessions.get(userSessionId); + if (entity != null && entity.getRealm().equals(realm.getId())) { + offlineUserSessions.remove(entity); + remove(entity, true); + } + } + + @Override + public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) { + ClientSessionEntity entity = new ClientSessionEntity(); + entity.setId(clientSession.getId()); + entity.setRealmId(clientSession.getRealm().getId()); + + entity.setAction(clientSession.getAction()); + entity.setAuthenticatorStatus(clientSession.getExecutionStatus()); + entity.setAuthMethod(clientSession.getAuthMethod()); + if (clientSession.getAuthenticatedUser() != null) { + entity.setAuthUserId(clientSession.getAuthenticatedUser().getId()); + } + entity.setClientId(clientSession.getClient().getId()); + if (clientSession.getNotes() != null) { + entity.getNotes().putAll(clientSession.getNotes()); + } + entity.setProtocolMappers(clientSession.getProtocolMappers()); + entity.setRedirectUri(clientSession.getRedirectUri()); + entity.setRoles(clientSession.getRoles()); + entity.setTimestamp(clientSession.getTimestamp()); + + if (clientSession.getUserSessionNotes() != null) { + entity.getUserSessionNotes().putAll(clientSession.getUserSessionNotes()); + } + + offlineClientSessions.put(clientSession.getId(), entity); + return new ClientSessionAdapter(session, this, clientSession.getRealm(), entity); + } + + @Override + public ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId) { + ClientSessionEntity entity = offlineClientSessions.get(clientSessionId); + if (entity != null && entity.getRealmId().equals(realm.getId())) { + return new ClientSessionAdapter(session, this, realm, entity); + } else { + return null; + } + } + + @Override + public List getOfflineClientSessions(RealmModel realm, UserModel user) { + List clientSessions = new LinkedList<>(); + for (UserSessionEntity s : this.offlineUserSessions.values()) { + if (s.getRealm().equals(realm.getId()) && s.getUser().equals(user.getId())) { + for (ClientSessionEntity cls : s.getClientSessions()) { + ClientSessionAdapter clAdapter = new ClientSessionAdapter(session, this, realm, cls); + clientSessions.add(clAdapter); + } + } + } + return clientSessions; + } + + @Override + public void removeOfflineClientSession(RealmModel realm, String clientSessionId) { + ClientSessionEntity entity = offlineClientSessions.get(clientSessionId); + if (entity != null && entity.getRealmId().equals(realm.getId())) { + offlineClientSessions.remove(entity.getId()); + UserSessionEntity userSession = entity.getSession(); + userSession.removeClientSession(entity); + } + } + + @Override + public int getOfflineSessionsCount(RealmModel realm, ClientModel client) { + return getUserSessions(realm, client, true).size(); + } + + @Override + public List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) { + return getUserSessions(realm, client, first, max, true); + } + @Override public void close() { } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java index 6a84b1a6e5..187a33ffbe 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java @@ -26,8 +26,12 @@ public class MemUserSessionProviderFactory { private final ConcurrentHashMap userSessionsByBrokerSessionId = new ConcurrentHashMap<>(); private final ConcurrentHashMap> userSessionsByBrokerUserId = new ConcurrentHashMap<>(); + private ConcurrentHashMap offlineUserSessions = new ConcurrentHashMap(); + private ConcurrentHashMap offlineClientSessions = new ConcurrentHashMap(); + public UserSessionProvider create(KeycloakSession session) { - return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures); + return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures, + offlineUserSessions, offlineClientSessions); } public void close() { diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java new file mode 100644 index 0000000000..450bbe14b2 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java @@ -0,0 +1,37 @@ +package org.keycloak.models.sessions.infinispan.compat; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.sessions.infinispan.initializer.SessionLoader; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public class SimpleUserSessionInitializer { + + private final KeycloakSessionFactory sessionFactory; + private final SessionLoader sessionLoader; + private final int sessionsPerSegment; + + public SimpleUserSessionInitializer(KeycloakSessionFactory sessionFactory, SessionLoader sessionLoader, int sessionsPerSegment) { + this.sessionFactory = sessionFactory; + this.sessionLoader = sessionLoader; + this.sessionsPerSegment = sessionsPerSegment; + } + + public void loadPersistentSessions() { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + int count = sessionLoader.getSessionsCount(session); + for (int i=0 ; i<=count ; i+=sessionsPerSegment) { + sessionLoader.loadSessions(session, i, sessionsPerSegment); + } + } + + }); + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java index a9db618583..c3bed676e0 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java @@ -39,6 +39,11 @@ public class UserSessionAdapter implements UserSessionModel { return entity.getId(); } + @Override + public RealmModel getRealm() { + return realm; + } + @Override public String getBrokerSessionId() { return entity.getBrokerSessionId(); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java index 5ddea31521..b260b331c7 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java @@ -22,8 +22,6 @@ public class ClientSessionEntity extends SessionEntity { private String redirectUri; - private String state; - private int timestamp; private String action; @@ -69,14 +67,6 @@ public class ClientSessionEntity extends SessionEntity { this.redirectUri = redirectUri; } - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - public int getTimestamp() { return timestamp; } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java new file mode 100644 index 0000000000..0d038bd6a2 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java @@ -0,0 +1,268 @@ +package org.keycloak.models.sessions.infinispan.initializer; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.infinispan.Cache; +import org.infinispan.context.Flag; +import org.infinispan.distexec.DefaultExecutorService; +import org.infinispan.distexec.DistributedExecutorService; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged; +import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent; +import org.infinispan.remoting.transport.Transport; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * Startup initialization for reading persistent userSessions/clientSessions to be filled into infinispan/memory . In cluster, + * the initialization is distributed among all cluster nodes, so the startup time is even faster + * + * @author Marek Posolda + */ +public class InfinispanUserSessionInitializer { + + private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class); + + private static final String STATE_KEY_PREFIX = "initializerState"; + + private final KeycloakSessionFactory sessionFactory; + private final Cache cache; + private final SessionLoader sessionLoader; + private final int maxErrors; + private final int sessionsPerSegment; + private final String stateKey; + + private volatile CountDownLatch latch = new CountDownLatch(1); + + + public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) { + this.sessionFactory = sessionFactory; + this.cache = cache; + this.sessionLoader = sessionLoader; + this.maxErrors = maxErrors; + this.sessionsPerSegment = sessionsPerSegment; + this.stateKey = STATE_KEY_PREFIX + "::" + stateKeySuffix; + } + + public void initCache() { + this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class); + cache.getCacheManager().addListener(new ViewChangeListener()); + } + + + public void loadPersistentSessions() { + if (isFinished()) { + return; + } + + while (!isFinished()) { + if (!isCoordinator()) { + try { + latch.await(500, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + log.error("Interrupted", ie); + } + } else { + startLoading(); + } + } + } + + + private boolean isFinished() { + InitializerState stateEntity = (InitializerState) cache.get(stateKey); + return stateEntity != null && stateEntity.isFinished(); + } + + + private InitializerState getOrCreateInitializerState() { + InitializerState state = (InitializerState) cache.get(stateKey); + if (state == null) { + final int[] count = new int[1]; + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + @Override + public void run(KeycloakSession session) { + count[0] = sessionLoader.getSessionsCount(session); + } + + }); + + state = new InitializerState(); + state.init(count[0], sessionsPerSegment); + saveStateToCache(state); + } + return state; + + } + + + private void saveStateToCache(final InitializerState state) { + + // 3 attempts to send the message (it may fail if some node fails in the meantime) + retry(3, new Runnable() { + + @Override + public void run() { + + // Save this synchronously to ensure all nodes read correct state + InfinispanUserSessionInitializer.this.cache.getAdvancedCache(). + withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS) + .put(stateKey, state); + } + + }); + } + + + private boolean isCoordinator() { + Transport transport = cache.getCacheManager().getTransport(); + return transport == null || transport.isCoordinator(); + } + + + // Just coordinator is supposed to run this + private void startLoading() { + InitializerState state = getOrCreateInitializerState(); + + // Assume each worker has same processor's count + int processors = Runtime.getRuntime().availableProcessors(); + + ExecutorService localExecutor = Executors.newCachedThreadPool(); + DistributedExecutorService distributedExecutorService = new DefaultExecutorService(cache, localExecutor); + + int errors = 0; + + try { + while (!state.isFinished()) { + Transport transport = cache.getCacheManager().getTransport(); + int nodesCount = transport==null ? 1 : transport.getMembers().size(); + int distributedWorkersCount = processors * nodesCount; + + // TODO: debug + log.infof("Starting next iteration with %d workers", distributedWorkersCount); + + List segments = state.getUnfinishedSegments(distributedWorkersCount); + + // TODO: trace + log.info("unfinished segments for this iteration: " + segments); + + List> futures = new LinkedList<>(); + for (Integer segment : segments) { + SessionInitializerWorker worker = new SessionInitializerWorker(); + worker.setWorkerEnvironment(segment, sessionsPerSegment, sessionLoader); + + Future future = distributedExecutorService.submit(worker); + futures.add(future); + } + + for (Future future : futures) { + try { + WorkerResult result = future.get(); + + if (result.getSuccess()) { + int computedSegment = result.getSegment(); + state.markSegmentFinished(computedSegment); + } else { + if (log.isTraceEnabled()) { + log.tracef("Segment %d failed to compute", result.getSegment()); + } + } + } catch (InterruptedException ie) { + errors++; + log.error("Interruped exception when computed future. Errors: " + errors, ie); + } catch (ExecutionException ee) { + errors++; + log.error("ExecutionException when computed future. Errors: " + errors, ee); + } + } + + if (errors >= maxErrors) { + throw new RuntimeException("Maximum count of worker errors occured. Limit was " + maxErrors + ". See server.log for details"); + } + + saveStateToCache(state); + + // TODO + log.info("New initializer state pushed. The state is: " + state.printState(false)); + } + } finally { + distributedExecutorService.shutdown(); + localExecutor.shutdown(); + } + } + + private void retry(int retry, Runnable runnable) { + while (true) { + try { + runnable.run(); + return; + } catch (RuntimeException e) { + retry--; + if (retry == 0) { + throw e; + } + } + } + } + + + @Listener + public class ViewChangeListener { + + @ViewChanged + public void viewChanged(ViewChangedEvent event) { + boolean isCoordinator = isCoordinator(); + // TODO: + log.info("View Changed: is coordinator: " + isCoordinator); + + if (isCoordinator) { + latch.countDown(); + latch = new CountDownLatch(1); + } + } + + } + + + public static class WorkerResult implements Serializable { + + private Integer segment; + private Boolean success; + + public static WorkerResult create (Integer segment, boolean success) { + WorkerResult res = new WorkerResult(); + res.setSegment(segment); + res.setSuccess(success); + return res; + } + + public Integer getSegment() { + return segment; + } + + public void setSegment(Integer segment) { + this.segment = segment; + } + + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java new file mode 100644 index 0000000000..eda7370b42 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java @@ -0,0 +1,108 @@ +package org.keycloak.models.sessions.infinispan.initializer; + +import java.util.ArrayList; +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * @author Marek Posolda + */ +public class InitializerState extends SessionEntity { + + private static final Logger log = Logger.getLogger(InitializerState.class); + + private int sessionsCount; + private List segments = new ArrayList<>(); + + + public void init(int sessionsCount, int sessionsPerSegment) { + this.sessionsCount = sessionsCount; + + int segmentsCount = sessionsCount / sessionsPerSegment; + if (sessionsPerSegment * segmentsCount < sessionsCount) { + segmentsCount = segmentsCount + 1; + } + + // TODO: trace + log.info(String.format("sessionsCount: %d, sessionsPerSegment: %d, segmentsCount: %d", sessionsCount, sessionsPerSegment, segmentsCount)); + + for (int i=0 ; i getUnfinishedSegments(int segmentCount) { + List result = new ArrayList<>(); + boolean remaining = true; + int next=0; + while (remaining && result.size() < segmentCount) { + next = getNextUnfinishedSegmentFromIndex(next); + if (next == -1) { + remaining = false; + } else { + result.add(next); + next++; + } + } + + return result; + } + + public void markSegmentFinished(int index) { + segments.set(index, true); + } + + private int getNextUnfinishedSegmentFromIndex(int index) { + int segmentsSize = segments.size(); + for (int i=index ; i finishedList = new ArrayList<>(); + List nonFinishedList = new ArrayList<>(); + + int size = segments.size(); + for (int i=0 ; iMarek Posolda + */ +public class OfflineUserSessionLoader implements SessionLoader { + + @Override + public int getSessionsCount(KeycloakSession session) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + return persister.getUserSessionsCount(true); + } + + @Override + public boolean loadSessions(KeycloakSession session, int first, int max) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + List sessions = persister.loadUserSessions(first, max, true); + + // TODO: Each worker may have different time. Improve if needed... + int currentTime = Time.currentTime(); + + for (UserSessionModel persistentSession : sessions) { + + // Update and persist lastSessionRefresh time + persistentSession.setLastSessionRefresh(currentTime); + persister.updateUserSession(persistentSession, true); + + // Save to memory/infinispan + UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(persistentSession); + + for (ClientSessionModel persistentClientSession : persistentSession.getClientSessions()) { + ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(persistentClientSession); + offlineClientSession.setUserSession(offlineUserSession); + } + } + + return true; + } + + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java new file mode 100644 index 0000000000..98fa69ec97 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java @@ -0,0 +1,65 @@ +package org.keycloak.models.sessions.infinispan.initializer; + +import java.io.Serializable; +import java.util.Set; + +import org.infinispan.Cache; +import org.infinispan.distexec.DistributedCallable; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public class SessionInitializerWorker implements DistributedCallable, Serializable { + + private static final Logger log = Logger.getLogger(SessionInitializerWorker.class); + + private int segment; + private int sessionsPerSegment; + private SessionLoader sessionLoader; + + private transient Cache cache; + + public void setWorkerEnvironment(int segment, int sessionsPerSegment, SessionLoader sessionLoader) { + this.segment = segment; + this.sessionsPerSegment = sessionsPerSegment; + this.sessionLoader = sessionLoader; + } + + @Override + public void setEnvironment(Cache cache, Set inputKeys) { + this.cache = cache; + } + + @Override + public InfinispanUserSessionInitializer.WorkerResult call() throws Exception { + // TODO + log.infof("Running computation for segment: %d", segment); + + KeycloakSessionFactory sessionFactory = cache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class); + if (sessionFactory == null) { + log.warnf("KeycloakSessionFactory not yet set in cache. Worker skipped"); + return InfinispanUserSessionInitializer.WorkerResult.create(segment, false); + } + + final int first = segment * sessionsPerSegment; + final int max = sessionsPerSegment; + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + sessionLoader.loadSessions(session, first, max); + } + + }); + + return InfinispanUserSessionInitializer.WorkerResult.create(segment, true); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java new file mode 100644 index 0000000000..5014147284 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java @@ -0,0 +1,15 @@ +package org.keycloak.models.sessions.infinispan.initializer; + +import java.io.Serializable; + +import org.keycloak.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public interface SessionLoader extends Serializable { + + int getSessionsCount(KeycloakSession session); + + boolean loadSessions(KeycloakSession session, int first, int max); +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java new file mode 100644 index 0000000000..8300944b37 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java @@ -0,0 +1,44 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import java.io.Serializable; +import java.util.Collection; + +import org.infinispan.distexec.mapreduce.Collector; +import org.infinispan.distexec.mapreduce.Mapper; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * Return all clientSessions attached to any from input list of userSessions + * + * @author Marek Posolda + */ +public class ClientSessionsOfUserSessionMapper implements Mapper, Serializable { + + private String realm; + private Collection userSessions; + + public ClientSessionsOfUserSessionMapper(String realm, Collection userSessions) { + this.realm = realm; + this.userSessions = userSessions; + } + + @Override + public void map(String key, SessionEntity e, Collector collector) { + if (!realm.equals(e.getRealm())) { + return; + } + + if (!(e instanceof ClientSessionEntity)) { + return; + } + + ClientSessionEntity entity = (ClientSessionEntity) e; + + for (String userSessionId : userSessions) { + if (userSessionId.equals(((ClientSessionEntity) e).getUserSession())) { + collector.emit(entity.getId(), entity); + } + } + } +} diff --git a/model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java b/model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java new file mode 100644 index 0000000000..de48a4247c --- /dev/null +++ b/model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java @@ -0,0 +1,45 @@ +package org.keycloak.models.sessions.infinispan.initializer; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Marek Posolda + */ +public class InitializerStateTest { + + @Test + public void testComputationState() { + InitializerState state = new InitializerState(); + state.init(28, 5); + + Assert.assertFalse(state.isFinished()); + List segments = state.getUnfinishedSegments(3); + assertContains(segments, 3, 0, 1, 2); + + state.markSegmentFinished(1); + state.markSegmentFinished(2); + segments = state.getUnfinishedSegments(4); + assertContains(segments, 4, 0, 3, 4, 5); + + state.markSegmentFinished(0); + state.markSegmentFinished(3); + segments = state.getUnfinishedSegments(4); + assertContains(segments, 2, 4, 5); + + state.markSegmentFinished(4); + state.markSegmentFinished(5); + segments = state.getUnfinishedSegments(4); + Assert.assertTrue(segments.isEmpty()); + Assert.assertTrue(state.isFinished()); + } + + private void assertContains(List segments, int expectedLength, int... expected) { + Assert.assertEquals(segments.size(), expectedLength); + for (int i : expected) { + Assert.assertTrue(segments.contains(i)); + } + } +} 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 8716d73f34..e899186c81 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -32,7 +32,7 @@ import org.keycloak.representations.RefreshToken; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.services.offline.OfflineTokenUtils; +import org.keycloak.services.managers.UserSessionManager; import org.keycloak.util.TokenUtil; import org.keycloak.util.Time; @@ -98,7 +98,7 @@ public class TokenManager { ClientSessionModel clientSession = null; if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { - clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState()); + clientSession = new UserSessionManager(session).findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState()); if (clientSession != null) { userSession = clientSession.getUserSession(); } @@ -490,14 +490,15 @@ public class TokenManager { String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM); boolean offlineTokenRequested = TokenUtil.isOfflineTokenRequested(scopeParam); if (offlineTokenRequested) { - if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) { + UserSessionManager sessionManager = new UserSessionManager(session); + if (!sessionManager.isOfflineTokenAllowed(clientSession)) { event.error(Errors.NOT_ALLOWED); throw new ErrorResponseException("not_allowed", "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST); } refreshToken = new RefreshToken(accessToken); refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE); - OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession); + sessionManager.persistOfflineSession(clientSession, userSession); } else { refreshToken = new RefreshToken(accessToken); refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout()); diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index afe2bdd740..edad00273c 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -11,6 +11,7 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; @@ -54,6 +55,11 @@ public class ClientManager { sessions.onClientRemoved(realm, client); } + UserSessionPersisterProvider sessionsPersister = realmManager.getSession().getProvider(UserSessionPersisterProvider.class); + if (sessionsPersister != null) { + sessionsPersister.onClientRemoved(realm, client); + } + UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client); if (serviceAccountUser != null) { realmManager.getSession().users().removeUser(realm, serviceAccountUser); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 749c8d83d3..d60d2effc7 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -19,6 +19,7 @@ package org.keycloak.services.managers; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.enums.SslRequired; +import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.utils.RealmImporter; import org.keycloak.models.AccountRoles; import org.keycloak.models.AdminRoles; @@ -197,6 +198,11 @@ public class RealmManager implements RealmImporter { sessions.onRealmRemoved(realm); } + UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class); + if (sessionsPersister != null) { + sessionsPersister.onRealmRemoved(realm); + } + // Remove all periodic syncs for configured federation providers UsersSyncManager usersSyncManager = new UsersSyncManager(); for (final UserFederationProviderModel fedProvider : federationProviders) { diff --git a/services/src/main/java/org/keycloak/services/managers/UserManager.java b/services/src/main/java/org/keycloak/services/managers/UserManager.java index 3c0252c2f0..686aedb576 100755 --- a/services/src/main/java/org/keycloak/services/managers/UserManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserManager.java @@ -4,6 +4,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; /** * @author Stian Thorgersen @@ -21,6 +22,12 @@ public class UserManager { if (sessions != null) { sessions.onUserRemoved(realm, user); } + + UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class); + if (sessionsPersister != null) { + sessionsPersister.onUserRemoved(realm, user); + } + if (session.users().removeUser(realm, user)) { return true; } diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java new file mode 100644 index 0000000000..5759d2031c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java @@ -0,0 +1,139 @@ +package org.keycloak.services.managers; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.session.UserSessionPersisterProvider; + +/** + * + * @author Marek Posolda + */ +public class UserSessionManager { + + protected static Logger logger = Logger.getLogger(UserSessionManager.class); + + private final KeycloakSession kcSession; + private final UserSessionPersisterProvider persister; + + public UserSessionManager(KeycloakSession session) { + this.kcSession = session; + this.persister = session.getProvider(UserSessionPersisterProvider.class); + } + + public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) { + UserModel user = userSession.getUser(); + + // Verify if we already have UserSession with this ID. If yes, don't create another one + UserSessionModel offlineUserSession = kcSession.sessions().getOfflineUserSession(clientSession.getRealm(), userSession.getId()); + if (offlineUserSession == null) { + offlineUserSession = createOfflineUserSession(user, userSession); + } + + // Create clientSession and save to DB. + createOfflineClientSession(user, clientSession, offlineUserSession); + } + + // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation + public ClientSessionModel findOfflineClientSession(RealmModel realm, String clientSessionId, String userSessionId) { + ClientSessionModel clientSession = kcSession.sessions().getOfflineClientSession(realm, clientSessionId); + if (clientSession == null) { + return null; + } + + if (!userSessionId.equals(clientSession.getUserSession().getId())) { + throw new ModelException("User session don't match. Offline client session " + clientSession.getId() + ", It's user session " + clientSession.getUserSession().getId() + + " Wanted user session: " + userSessionId); + } + + return clientSession; + } + + public Set findClientsWithOfflineToken(RealmModel realm, UserModel user) { + List clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user); + Set clients = new HashSet<>(); + for (ClientSessionModel clientSession : clientSessions) { + clients.add(clientSession.getClient()); + } + return clients; + } + + public boolean revokeOfflineToken(UserModel user, ClientModel client) { + RealmModel realm = client.getRealm(); + + List clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user); + boolean anyRemoved = false; + for (ClientSessionModel clientSession : clientSessions) { + if (clientSession.getClient().getId().equals(client.getId())) { + if (logger.isTraceEnabled()) { + logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .", + user.getUsername(), client.getClientId(), clientSession.getId()); + } + + kcSession.sessions().removeOfflineClientSession(realm, clientSession.getId()); + persister.removeClientSession(clientSession.getId(), true); + checkOfflineUserSessionHasClientSessions(realm, user, clientSession.getUserSession().getId(), clientSessions); + anyRemoved = true; + } + } + + return anyRemoved; + } + + public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) { + RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE); + if (offlineAccessRole == null) { + logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE); + return false; + } + + return clientSession.getRoles().contains(offlineAccessRole.getId()); + } + + private UserSessionModel createOfflineUserSession(UserModel user, UserSessionModel userSession) { + if (logger.isTraceEnabled()) { + logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername()); + } + + UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession); + persister.createUserSession(userSession, true); + return offlineUserSession; + } + + private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) { + if (logger.isTraceEnabled()) { + logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" , + clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId()); + } + + ClientSessionModel offlineClientSession = kcSession.sessions().createOfflineClientSession(clientSession); + offlineClientSession.setUserSession(userSession); + persister.createClientSession(clientSession, true); + } + + // Check if userSession has any offline clientSessions attached to it. Remove userSession if not + private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, String userSessionId, List clientSessions) { + for (ClientSessionModel clientSession : clientSessions) { + if (clientSession.getUserSession().getId().equals(userSessionId)) { + return; + } + } + + if (logger.isTraceEnabled()) { + logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId); + } + kcSession.sessions().removeOfflineUserSession(realm, userSessionId); + persister.removeUserSession(userSessionId, true); + } +} diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java deleted file mode 100644 index 519ae259f0..0000000000 --- a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java +++ /dev/null @@ -1,300 +0,0 @@ -package org.keycloak.services.offline; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.codehaus.jackson.annotate.JsonProperty; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.ModelException; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.util.JsonSerialization; - -/** - * @author Marek Posolda - */ -public class OfflineClientSessionAdapter implements ClientSessionModel { - - private final OfflineClientSessionModel model; - private final RealmModel realm; - private final ClientModel client; - private final OfflineUserSessionAdapter userSession; - - private OfflineClientSessionData data; - - public OfflineClientSessionAdapter(OfflineClientSessionModel model, RealmModel realm, ClientModel client, OfflineUserSessionAdapter userSession) { - this.model = model; - this.realm = realm; - this.client = client; - this.userSession = userSession; - } - - // lazily init representation - private OfflineClientSessionData getData() { - if (data == null) { - try { - data = JsonSerialization.readValue(model.getData(), OfflineClientSessionData.class); - } catch (IOException ioe) { - throw new ModelException(ioe); - } - } - - return data; - } - - @Override - public String getId() { - return model.getClientSessionId(); - } - - @Override - public RealmModel getRealm() { - return realm; - } - - @Override - public ClientModel getClient() { - return client; - } - - @Override - public UserSessionModel getUserSession() { - return userSession; - } - - @Override - public void setUserSession(UserSessionModel userSession) { - throw new IllegalStateException("Not supported setUserSession"); - } - - @Override - public String getRedirectUri() { - return getData().getRedirectUri(); - } - - @Override - public void setRedirectUri(String uri) { - throw new IllegalStateException("Not supported setRedirectUri"); - } - - @Override - public int getTimestamp() { - return getData().getTimestamp(); - } - - @Override - public void setTimestamp(int timestamp) { - throw new IllegalStateException("Not supported setTimestamp"); - } - - @Override - public String getAction() { - return null; - } - - @Override - public void setAction(String action) { - throw new IllegalStateException("Not supported setAction"); - } - - @Override - public Set getRoles() { - return getData().getRoles(); - } - - @Override - public void setRoles(Set roles) { - throw new IllegalStateException("Not supported setRoles"); - } - - @Override - public Set getProtocolMappers() { - return getData().getProtocolMappers(); - } - - @Override - public void setProtocolMappers(Set protocolMappers) { - throw new IllegalStateException("Not supported setProtocolMappers"); - } - - @Override - public Map getExecutionStatus() { - return getData().getAuthenticatorStatus(); - } - - @Override - public void setExecutionStatus(String authenticator, ExecutionStatus status) { - throw new IllegalStateException("Not supported setExecutionStatus"); - } - - @Override - public void clearExecutionStatus() { - throw new IllegalStateException("Not supported clearExecutionStatus"); - } - - @Override - public UserModel getAuthenticatedUser() { - return userSession.getUser(); - } - - @Override - public void setAuthenticatedUser(UserModel user) { - throw new IllegalStateException("Not supported setAuthenticatedUser"); - } - - @Override - public String getAuthMethod() { - return getData().getAuthMethod(); - } - - @Override - public void setAuthMethod(String method) { - throw new IllegalStateException("Not supported setAuthMethod"); - } - - @Override - public String getNote(String name) { - return getData().getNotes()==null ? null : getData().getNotes().get(name); - } - - @Override - public void setNote(String name, String value) { - throw new IllegalStateException("Not supported setNote"); - } - - @Override - public void removeNote(String name) { - throw new IllegalStateException("Not supported removeNote"); - } - - @Override - public Map getNotes() { - return getData().getNotes(); - } - - @Override - public Set getRequiredActions() { - throw new IllegalStateException("Not supported getRequiredActions"); - } - - @Override - public void addRequiredAction(String action) { - throw new IllegalStateException("Not supported addRequiredAction"); - } - - @Override - public void removeRequiredAction(String action) { - throw new IllegalStateException("Not supported removeRequiredAction"); - } - - @Override - public void addRequiredAction(UserModel.RequiredAction action) { - throw new IllegalStateException("Not supported addRequiredAction"); - } - - @Override - public void removeRequiredAction(UserModel.RequiredAction action) { - throw new IllegalStateException("Not supported removeRequiredAction"); - } - - @Override - public void setUserSessionNote(String name, String value) { - throw new IllegalStateException("Not supported setUserSessionNote"); - } - - @Override - public Map getUserSessionNotes() { - throw new IllegalStateException("Not supported getUserSessionNotes"); - } - - @Override - public void clearUserSessionNotes() { - throw new IllegalStateException("Not supported clearUserSessionNotes"); - } - - protected static class OfflineClientSessionData { - - @JsonProperty("authMethod") - private String authMethod; - - @JsonProperty("redirectUri") - private String redirectUri; - - @JsonProperty("protocolMappers") - private Set protocolMappers; - - @JsonProperty("roles") - private Set roles; - - @JsonProperty("notes") - private Map notes; - - @JsonProperty("authenticatorStatus") - private Map authenticatorStatus = new HashMap<>(); - - @JsonProperty("timestamp") - private int timestamp; - - public String getAuthMethod() { - return authMethod; - } - - public void setAuthMethod(String authMethod) { - this.authMethod = authMethod; - } - - public String getRedirectUri() { - return redirectUri; - } - - public void setRedirectUri(String redirectUri) { - this.redirectUri = redirectUri; - } - - public Set getProtocolMappers() { - return protocolMappers; - } - - public void setProtocolMappers(Set protocolMappers) { - this.protocolMappers = protocolMappers; - } - - public Set getRoles() { - return roles; - } - - public void setRoles(Set roles) { - this.roles = roles; - } - - public Map getNotes() { - return notes; - } - - public void setNotes(Map notes) { - this.notes = notes; - } - - public Map getAuthenticatorStatus() { - return authenticatorStatus; - } - - public void setAuthenticatorStatus(Map authenticatorStatus) { - this.authenticatorStatus = authenticatorStatus; - } - - public int getTimestamp() { - return timestamp; - } - - public void setTimestamp(int timestamp) { - this.timestamp = timestamp; - } - } -} diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java deleted file mode 100644 index 13ff55b6dd..0000000000 --- a/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.keycloak.services.offline; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import org.jboss.logging.Logger; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelException; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.util.JsonSerialization; -import org.keycloak.util.Time; - -/** - * - * @author Marek Posolda - */ -public class OfflineTokenUtils { - - protected static Logger logger = Logger.getLogger(OfflineTokenUtils.class); - - public static void persistOfflineSession(KeycloakSession kcSession, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession) { - UserModel user = userSession.getUser(); - ClientModel client = clientSession.getClient(); - - // First verify if we already have offlineToken for this user+client . If yes, then invalidate it (This is to avoid leaks) - Collection clientSessions = kcSession.users().getOfflineClientSessions(realm, user); - for (OfflineClientSessionModel existing : clientSessions) { - if (existing.getClientId().equals(client.getId())) { - if (logger.isTraceEnabled()) { - logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' . Offline token will be replaced with new one", - user.getUsername(), client.getClientId(), existing.getClientSessionId()); - } - - kcSession.users().removeOfflineClientSession(realm, user, existing.getClientSessionId()); - - // Check if userSession is ours. If not, then check if it has other clientSessions and remove it otherwise - if (!existing.getUserSessionId().equals(userSession.getId())) { - checkUserSessionHasClientSessions(kcSession, realm, user, existing.getUserSessionId()); - } - } - } - - // Verify if we already have UserSession with this ID. If yes, don't create another one - OfflineUserSessionModel userSessionRep = kcSession.users().getOfflineUserSession(realm, user, userSession.getId()); - if (userSessionRep == null) { - createOfflineUserSession(kcSession, realm, user, userSession); - } - - // Create clientRep and save to DB. - createOfflineClientSession(kcSession, realm, user, clientSession, userSession); - } - - // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation - public static ClientSessionModel findOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, String clientSessionId, String userSessionId) { - OfflineClientSessionModel clientSession = kcSession.users().getOfflineClientSession(realm, user, clientSessionId); - if (clientSession == null) { - return null; - } - - if (!userSessionId.equals(clientSession.getUserSessionId())) { - throw new ModelException("User session don't match. Offline client session " + clientSession.getClientSessionId() + ", It's user session " + clientSession.getUserSessionId() + - " Wanted user session: " + userSessionId); - } - - OfflineUserSessionModel userSession = kcSession.users().getOfflineUserSession(realm, user, userSessionId); - if (userSession == null) { - throw new ModelException("Found clientSession " + clientSessionId + " but not userSession " + userSessionId); - } - - OfflineUserSessionAdapter userSessionAdapter = new OfflineUserSessionAdapter(userSession, user); - - ClientModel client = realm.getClientById(clientSession.getClientId()); - return new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter); - } - - public static Set findClientsWithOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user) { - Collection clientSessions = kcSession.users().getOfflineClientSessions(realm, user); - Set clients = new HashSet<>(); - for (OfflineClientSessionModel clientSession : clientSessions) { - ClientModel client = realm.getClientById(clientSession.getClientId()); - clients.add(client); - } - return clients; - } - - public static boolean revokeOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientModel client) { - Collection clientSessions = kcSession.users().getOfflineClientSessions(realm, user); - boolean anyRemoved = false; - for (OfflineClientSessionModel clientSession : clientSessions) { - if (clientSession.getClientId().equals(client.getId())) { - if (logger.isTraceEnabled()) { - logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .", - user.getUsername(), client.getClientId(), clientSession.getClientSessionId()); - } - - kcSession.users().removeOfflineClientSession(realm, user, clientSession.getClientSessionId()); - checkUserSessionHasClientSessions(kcSession, realm, user, clientSession.getUserSessionId()); - anyRemoved = true; - } - } - - return anyRemoved; - } - - public static boolean isOfflineTokenAllowed(RealmModel realm, ClientSessionModel clientSession) { - RoleModel offlineAccessRole = realm.getRole(Constants.OFFLINE_ACCESS_ROLE); - if (offlineAccessRole == null) { - logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE); - return false; - } - - return clientSession.getRoles().contains(offlineAccessRole.getId()); - } - - private static void createOfflineUserSession(KeycloakSession kcSession, RealmModel realm, UserModel user, UserSessionModel userSession) { - if (logger.isTraceEnabled()) { - logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername()); - } - OfflineUserSessionAdapter.OfflineUserSessionData rep = new OfflineUserSessionAdapter.OfflineUserSessionData(); - rep.setBrokerUserId(userSession.getBrokerUserId()); - rep.setBrokerSessionId(userSession.getBrokerSessionId()); - rep.setIpAddress(userSession.getIpAddress()); - rep.setAuthMethod(userSession.getAuthMethod()); - rep.setRememberMe(userSession.isRememberMe()); - rep.setStarted(userSession.getStarted()); - rep.setNotes(userSession.getNotes()); - - try { - String stringRep = JsonSerialization.writeValueAsString(rep); - OfflineUserSessionModel sessionModel = new OfflineUserSessionModel(); - sessionModel.setUserSessionId(userSession.getId()); - sessionModel.setData(stringRep); - kcSession.users().addOfflineUserSession(realm, user, sessionModel); - } catch (IOException ioe) { - throw new ModelException(ioe); - } - } - - private static void createOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) { - if (logger.isTraceEnabled()) { - logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" , - clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId()); - } - OfflineClientSessionAdapter.OfflineClientSessionData rep = new OfflineClientSessionAdapter.OfflineClientSessionData(); - rep.setAuthMethod(clientSession.getAuthMethod()); - rep.setRedirectUri(clientSession.getRedirectUri()); - rep.setProtocolMappers(clientSession.getProtocolMappers()); - rep.setRoles(clientSession.getRoles()); - rep.setNotes(clientSession.getNotes()); - rep.setAuthenticatorStatus(clientSession.getExecutionStatus()); - rep.setTimestamp(Time.currentTime()); - - try { - String stringRep = JsonSerialization.writeValueAsString(rep); - OfflineClientSessionModel clsModel = new OfflineClientSessionModel(); - clsModel.setClientSessionId(clientSession.getId()); - clsModel.setClientId(clientSession.getClient().getId()); - clsModel.setUserId(user.getId()); - clsModel.setUserSessionId(userSession.getId()); - clsModel.setData(stringRep); - kcSession.users().addOfflineClientSession(realm, clsModel); - } catch (IOException ioe) { - throw new ModelException(ioe); - } - } - - // Check if userSession has any offline clientSessions attached to it. Remove userSession if not - private static void checkUserSessionHasClientSessions(KeycloakSession kcSession, RealmModel realm, UserModel user, String userSessionId) { - Collection clientSessions = kcSession.users().getOfflineClientSessions(realm, user); - - for (OfflineClientSessionModel clientSession : clientSessions) { - if (clientSession.getUserSessionId().equals(userSessionId)) { - return; - } - } - - if (logger.isTraceEnabled()) { - logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId); - } - kcSession.users().removeOfflineUserSession(realm, user, userSessionId); - } -} diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 17de6a2225..f130e50688 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -56,8 +56,8 @@ import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.messages.Messages; -import org.keycloak.services.offline.OfflineTokenUtils; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; import org.keycloak.util.UriUtils; @@ -486,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService { // Revoke grant in UserModel UserModel user = auth.getUser(); user.revokeConsentForClient(client.getId()); - OfflineTokenUtils.revokeOfflineToken(session, realm, user, client); + new UserSessionManager(session).revokeOfflineToken(user, client); // Logout clientSessions for this user and client AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 71d8ac0be1..46e2a096a3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -7,11 +7,8 @@ import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; @@ -27,8 +24,6 @@ import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; -import org.keycloak.services.offline.OfflineClientSessionAdapter; -import org.keycloak.services.offline.OfflineUserSessionAdapter; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.ErrorResponse; import org.keycloak.util.JsonSerialization; @@ -413,7 +408,7 @@ public class ClientResource { public Map getOfflineSessionCount() { auth.requireView(); Map map = new HashMap(); - map.put("count", session.users().getOfflineClientSessionsCount(client.getRealm(), client)); + map.put("count", session.sessions().getOfflineSessionsCount(client.getRealm(), client)); return map; } @@ -435,19 +430,9 @@ public class ClientResource { firstResult = firstResult != null ? firstResult : -1; maxResults = maxResults != null ? maxResults : -1; List sessions = new ArrayList(); - for (OfflineClientSessionModel offlineClientSession : session.users().getOfflineClientSessions(client.getRealm(), client, firstResult, maxResults)) { - UserModel user = session.users().getUserById(offlineClientSession.getUserId(), client.getRealm()); - OfflineUserSessionModel offlineUserSession = session.users().getOfflineUserSession(client.getRealm(), user, offlineClientSession.getUserSessionId()); - OfflineUserSessionAdapter sessionAdapter = new OfflineUserSessionAdapter(offlineUserSession, user); - OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(offlineClientSession, client.getRealm(), client, sessionAdapter); - - UserSessionRepresentation rep = new UserSessionRepresentation(); - rep.setId(sessionAdapter.getId()); - rep.setStart(Time.toMillis(clientSessionAdapter.getTimestamp())); - rep.setUsername(user.getUsername()); - rep.setUserId(user.getId()); - rep.setIpAddress(sessionAdapter.getIpAddress()); - + List userSessions = session.sessions().getOfflineUserSessions(client.getRealm(), client, firstResult, maxResults); + for (UserSessionModel userSession : userSessions) { + UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); sessions.add(rep); } return sessions; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index bf535d9f87..22ebbb5c11 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -77,7 +77,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.services.managers.BruteForceProtector; -import org.keycloak.services.offline.OfflineTokenUtils; +import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.resources.AccountService; /** @@ -451,7 +451,7 @@ public class UsersResource { List> result = new LinkedList<>(); - Set offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user); + Set offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user); for (ClientModel client : realm.getClients()) { UserConsentModel consent = user.getConsentByClient(client.getId()); @@ -496,7 +496,7 @@ public class UsersResource { ClientModel client = realm.getClientByClientId(clientId); boolean revokedConsent = user.revokeConsentForClient(client.getId()); - boolean revokedOfflineToken = OfflineTokenUtils.revokeOfflineToken(session, realm, user, client); + boolean revokedOfflineToken = new UserSessionManager(session).revokeOfflineToken(user, client); if (revokedConsent) { // Logout clientSessions for this user and client diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index c760152991..4d4c2f6957 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -22,6 +22,10 @@ "provider": "${keycloak.user.provider:jpa}" }, + "userSessionPersister": { + "provider": "${keycloak.userSessionPersister.provider:jpa}" + }, + "userSessions": { "provider" : "${keycloak.userSessions.provider:infinispan}" }, diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java new file mode 100644 index 0000000000..0eabbe1ea2 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java @@ -0,0 +1,226 @@ +package org.keycloak.testsuite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; + +import org.infinispan.AdvancedCache; +import org.infinispan.Cache; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.util.Time; + +/** + * HOWTO USE THIS: + * + * 1) Run KeycloakServer with system properties (assuming mongo up and running on localhost): + * -Dkeycloak.realm.provider=mongo -Dkeycloak.user.provider=mongo -Dkeycloak.userSessionPersister.provider=mongo -Dkeycloak.connectionsMongo.db=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dresources -DstartInfinispanCLI + * + * 2) Write command on STDIN to persist 50000 userSessions to mongo: persistSessions 50000 + * + * 3) Run command "clear" to ensure infinispan cache is cleared. Doublecheck with command "size" is 0 + * + * 4) Write command to load sessions from persistent storage - 100 sessions per worker transaction: loadPersistentSessions 100 + * + * See the progress in log. Finally run command "size" to ensure size is 100001 (50000 userSessions + 50000 clientSessions + 1 initializationState item) + * + * 5) Alternative to step 3+4 - Kill the server after step 2 and start two KeycloakServer in parallel on ports 8081 and 8082 . See the progress in logs of loading persistent sessions to infinispan. + * Kill the coordinator (usually 8081 node) during startup and see the node 8082 became coordinator and took ownership of loading persistent sessions. After node 8082 fully started, the size of infinispan is again 100001 + * + * @author Marek Posolda + */ +public class InfinispanCLI { + + private static final Logger log = Logger.getLogger(InfinispanCLI.class); + + private final KeycloakSessionFactory sessionFactory; + + public InfinispanCLI(KeycloakServer server) { + this.sessionFactory = server.getSessionFactory(); + } + + // WARNING: Stdin blocking operation + public void start() throws IOException { + log.info("Starting infinispan CLI. Exit with 'exit'"); + + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String line; + try { + while ((line = reader.readLine()) != null) { + log.info("Command: " + line); + + if (line.equals("exit")) { + return; + } + + final String finalLine = line; + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + Cache ispnCache = provider.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + runTask(finalLine, ispnCache); + } + + }); + } + } finally { + log.info("Exit infinispan CLI"); + reader.close(); + } + } + + private void runTask(String line, Cache cache) { + try { + String[] splits = line.split(" "); + if (splits[0].equals("put")) { + UserSessionEntity userSession = new UserSessionEntity(); + String id = splits[1]; + + userSession.setId(id); + userSession.setRealm(splits[2]); + userSession.setLastSessionRefresh(Time.currentTime()); + cache.put(id, userSession); + + } else if (splits[0].equals("get")) { + String id = splits[1]; + UserSessionEntity userSession = (UserSessionEntity) cache.get(id); + printSession(id, userSession); + } else if (splits[0].equals("remove")) { + String id = splits[1]; + cache.remove(id); + } else if (splits[0].equals("clear")) { + cache.clear(); + log.info("Cache cleared"); + } else if (splits[0].equals("size")) { + log.info("Size: " + cache.size()); + } else if (splits[0].equals("list")) { + for (String id : cache.keySet()) { + SessionEntity entity = cache.get(id); + if (!(entity instanceof UserSessionEntity)) { + continue; + } + UserSessionEntity userSession = (UserSessionEntity) cache.get(id); + log.info("list: key=" + id + ", value=" + toString(userSession)); + } + + } else if (splits[0].equals("getLocal")) { + String id = splits[1]; + cache = ((AdvancedCache) cache).withFlags(Flag.CACHE_MODE_LOCAL); + UserSessionEntity userSession = (UserSessionEntity) cache.get(id); + printSession(id, userSession); + + } else if (splits[0].equals("persistSessions")) { + + final int count = Integer.parseInt(splits[1]); + final List userSessionIds = new LinkedList<>(); + final List clientSessionIds = new LinkedList<>(); + + // Create sessions in separate transaction first + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName("master"); + UserModel john = session.users().getUserByUsername("admin", realm); + ClientModel testApp = realm.getClientByClientId("security-admin-console"); + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + + for (int i=0 ; i offlineUserSessions = session.users().getOfflineUserSessions(realm, admin); - Collection offlineClientSessions = session.users().getOfflineClientSessions(realm, admin); - Assert.assertEquals(offlineUserSessions.size(), 1); - Assert.assertEquals(offlineClientSessions.size(), 1); - OfflineUserSessionModel offlineSession = offlineUserSessions.iterator().next(); - OfflineClientSessionModel offlineClSession = offlineClientSessions.iterator().next(); - Assert.assertEquals(offlineSession.getData(), "something1"); - Assert.assertEquals(offlineSession.getUserSessionId(), "123"); - Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId()); - Assert.assertEquals(offlineClSession.getUserSessionId(), "123"); - Assert.assertEquals(offlineClSession.getUserId(), admin.getId()); - Assert.assertEquals(offlineClSession.getData(), "something2"); +// // Test offline sessions +// Collection offlineUserSessions = session.users().getOfflineUserSessions(realm, admin); +// Collection offlineClientSessions = session.users().getOfflineClientSessions(realm, admin); +// Assert.assertEquals(offlineUserSessions.size(), 1); +// Assert.assertEquals(offlineClientSessions.size(), 1); +// PersistentUserSessionModel offlineSession = offlineUserSessions.iterator().next(); +// PersistentClientSessionModel offlineClSession = offlineClientSessions.iterator().next(); +// Assert.assertEquals(offlineSession.getData(), "something1"); +// Assert.assertEquals(offlineSession.getUserSessionId(), "123"); +// Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId()); +// Assert.assertEquals(offlineClSession.getUserSessionId(), "123"); +// Assert.assertEquals(offlineClSession.getUserId(), admin.getId()); +// Assert.assertEquals(offlineClSession.getData(), "something2"); // Test service accounts diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index 68fa2f8a3b..fee7bdee53 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -4,8 +4,8 @@ import org.junit.Assert; import org.junit.Test; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OfflineClientSessionModel; -import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.session.PersistentClientSessionModel; +import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; @@ -286,82 +286,82 @@ public class UserModelTest extends AbstractModelTest { Assert.assertNull(session.users().getUserByUsername("user1", realm)); } - @Test - public void testOfflineSessionsRemoved() { - RealmModel realm = realmManager.createRealm("original"); - ClientModel fooClient = realm.addClient("foo"); - ClientModel barClient = realm.addClient("bar"); - - UserModel user1 = session.users().addUser(realm, "user1"); - UserModel user2 = session.users().addUser(realm, "user2"); - - addOfflineUserSession(realm, user1, "123", "something1"); - addOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2"); - addOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3"); - - addOfflineUserSession(realm, user2, "2123", "something4"); - addOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5"); - - commit(); - - // Searching by clients - Assert.assertEquals(2, session.users().getOfflineClientSessionsCount(realm, fooClient)); - Assert.assertEquals(1, session.users().getOfflineClientSessionsCount(realm, barClient)); - - Collection clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10); - Assert.assertEquals(2, clientSessions.size()); - clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1); - OfflineClientSessionModel cls = clientSessions.iterator().next(); - assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2"); - clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1); - cls = clientSessions.iterator().next(); - assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5"); - - clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10); - Assert.assertEquals(1, clientSessions.size()); - cls = clientSessions.iterator().next(); - assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3"); - - realm = realmManager.getRealmByName("original"); - realm.removeClient(barClient.getId()); - - commit(); - - realm = realmManager.getRealmByName("original"); - user1 = session.users().getUserByUsername("user1", realm); - Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData()); - Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData()); - Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789")); - - realm.removeClient(fooClient.getId()); - - commit(); - - realm = realmManager.getRealmByName("original"); - user1 = session.users().getUserByUsername("user1", realm); - Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456")); - Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789")); - Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123")); - Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size()); - Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size()); - } - - private void addOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) { - OfflineUserSessionModel model = new OfflineUserSessionModel(); - model.setUserSessionId(userSessionId); - model.setData(data); - session.users().addOfflineUserSession(realm, user, model); - } - - private void addOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) { - OfflineClientSessionModel model = new OfflineClientSessionModel(); - model.setClientSessionId(clientSessionId); - model.setUserSessionId(userSessionId); - model.setUserId(user.getId()); - model.setClientId(clientId); - model.setData(data); - session.users().addOfflineClientSession(realm, model); - } +// @Test +// public void testOfflineSessionsRemoved() { +// RealmModel realm = realmManager.createRealm("original"); +// ClientModel fooClient = realm.addClient("foo"); +// ClientModel barClient = realm.addClient("bar"); +// +// UserModel user1 = session.users().addUser(realm, "user1"); +// UserModel user2 = session.users().addUser(realm, "user2"); +// +// createOfflineUserSession(realm, user1, "123", "something1"); +// createOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2"); +// createOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3"); +// +// createOfflineUserSession(realm, user2, "2123", "something4"); +// createOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5"); +// +// commit(); +// +// // Searching by clients +// Assert.assertEquals(2, session.users().getOfflineSessionsCount(realm, fooClient)); +// Assert.assertEquals(1, session.users().getOfflineSessionsCount(realm, barClient)); +// +// Collection clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10); +// Assert.assertEquals(2, clientSessions.size()); +// clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1); +// PersistentClientSessionModel cls = clientSessions.iterator().next(); +// assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2"); +// clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1); +// cls = clientSessions.iterator().next(); +// assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5"); +// +// clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10); +// Assert.assertEquals(1, clientSessions.size()); +// cls = clientSessions.iterator().next(); +// assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3"); +// +// realm = realmManager.getRealmByName("original"); +// realm.removeClient(barClient.getId()); +// +// commit(); +// +// realm = realmManager.getRealmByName("original"); +// user1 = session.users().getUserByUsername("user1", realm); +// Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData()); +// Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData()); +// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789")); +// +// realm.removeClient(fooClient.getId()); +// +// commit(); +// +// realm = realmManager.getRealmByName("original"); +// user1 = session.users().getUserByUsername("user1", realm); +// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456")); +// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789")); +// Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123")); +// Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size()); +// Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size()); +// } +// +// private void createOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) { +// PersistentUserSessionModel model = new PersistentUserSessionModel(); +// model.setUserSessionId(userSessionId); +// model.setData(data); +// session.users().createOfflineUserSession(realm, user, model); +// } +// +// private void createOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) { +// PersistentClientSessionModel model = new PersistentClientSessionModel(); +// model.setClientSessionId(clientSessionId); +// model.setUserSessionId(userSessionId); +// model.setUserId(user.getId()); +// model.setClientId(clientId); +// model.setData(data); +// session.users().createOfflineClientSession(realm, model); +// } public static void assertEquals(UserModel expected, UserModel actual) { Assert.assertEquals(expected.getUsername(), actual.getUsername()); @@ -377,7 +377,7 @@ public class UserModelTest extends AbstractModelTest { Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions); } - private static void assertSessionEquals(OfflineClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId, + private static void assertSessionEquals(PersistentClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId, String expectedClientId, String expectedUserId, String expectedData) { Assert.assertEquals(cls.getData(), expectedData); Assert.assertEquals(cls.getClientSessionId(), expectedClientSessionId); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java new file mode 100644 index 0000000000..cf7cc8ab42 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -0,0 +1,162 @@ +package org.keycloak.testsuite.model; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.managers.UserManager; +import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.util.Time; + +/** + * @author Marek Posolda + */ +public class UserSessionInitializerTest { + + @ClassRule + public static KeycloakRule kc = new KeycloakRule(); + + private KeycloakSession session; + private RealmModel realm; + private UserSessionManager sessionManager; + + @Before + public void before() { + session = kc.startSession(); + realm = session.realms().getRealm("test"); + session.users().addUser(realm, "user1").setEmail("user1@localhost"); + session.users().addUser(realm, "user2").setEmail("user2@localhost"); + sessionManager = new UserSessionManager(session); + } + + @After + public void after() { + resetSession(); + session.sessions().removeUserSessions(realm); + UserModel user1 = session.users().getUserByUsername("user1", realm); + UserModel user2 = session.users().getUserByUsername("user2", realm); + + UserManager um = new UserManager(session); + um.removeUser(realm, user1); + um.removeUser(realm, user2); + kc.stopSession(session, true); + } + + @Test + public void testUserSessionInitializer() { + UserSessionModel[] origSessions = createSessions(); + + resetSession(); + + // Create and persist offline sessions + for (UserSessionModel origSession : origSessions) { + UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId()); + for (ClientSessionModel clientSession : userSession.getClientSessions()) { + sessionManager.persistOfflineSession(clientSession, userSession); + } + } + + resetSession(); + + // Delete cache (persisted sessions are still kept) + session.sessions().onRealmRemoved(realm); + + // Clear ispn cache to ensure initializerState is removed as well + InfinispanConnectionProvider infinispan = session.getProvider(InfinispanConnectionProvider.class); + infinispan.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).clear(); + + resetSession(); + + ClientModel testApp = realm.getClientByClientId("test-app"); + ClientModel thirdparty = realm.getClientByClientId("third-party"); + Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, testApp)); + Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, thirdparty)); + + int started = Time.currentTime(); + + try { + // Set some offset to ensure lastSessionRefresh will be updated + Time.setOffset(10); + + // Load sessions from persister into infinispan/memory + UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class); + userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 10, 2); + + resetSession(); + + // Assert sessions are in + testApp = realm.getClientByClientId("test-app"); + Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp)); + Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty)); + + List loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10); + UserSessionProviderTest.assertSessions(loadedSessions, origSessions); + + UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started+10, "test-app", "third-party"); + UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app"); + UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started+10, "test-app"); + } finally { + Time.setOffset(0); + } + } + + private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + if (userSession != null) clientSession.setUserSession(userSession); + clientSession.setRedirectUri(redirect); + if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); + if (roles != null) clientSession.setRoles(roles); + if (protocolMappers != null) clientSession.setProtocolMappers(protocolMappers); + return clientSession; + } + + private UserSessionModel[] createSessions() { + UserSessionModel[] sessions = new UserSessionModel[3]; + sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + + Set roles = new HashSet(); + roles.add("one"); + roles.add("two"); + + Set protocolMappers = new HashSet(); + protocolMappers.add("mapper-one"); + protocolMappers.add("mapper-two"); + + createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); + createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); + + sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); + + sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); + + resetSession(); + + return sessions; + } + + private void resetSession() { + kc.stopSession(session, true); + session = kc.startSession(); + realm = session.realms().getRealm("test"); + sessionManager = new UserSessionManager(session); + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java new file mode 100644 index 0000000000..53480aa05f --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -0,0 +1,335 @@ +package org.keycloak.testsuite.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.managers.UserManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.util.Time; + +/** + * @author Marek Posolda + */ +public class UserSessionPersisterProviderTest { + + @ClassRule + public static KeycloakRule kc = new KeycloakRule(); + + private KeycloakSession session; + private RealmModel realm; + private UserSessionPersisterProvider persister; + + @Before + public void before() { + session = kc.startSession(); + realm = session.realms().getRealm("test"); + session.users().addUser(realm, "user1").setEmail("user1@localhost"); + session.users().addUser(realm, "user2").setEmail("user2@localhost"); + persister = session.getProvider(UserSessionPersisterProvider.class); + } + + @After + public void after() { + resetSession(); + session.sessions().removeUserSessions(realm); + UserModel user1 = session.users().getUserByUsername("user1", realm); + UserModel user2 = session.users().getUserByUsername("user2", realm); + + UserManager um = new UserManager(session); + um.removeUser(realm, user1); + um.removeUser(realm, user2); + kc.stopSession(session, true); + } + + @Test + public void testPersistenceWithLoad() { + // Create some sessions in infinispan + int started = Time.currentTime(); + UserSessionModel[] origSessions = createSessions(); + + resetSession(); + + // Persist 3 created userSessions and clientSessions as offline + ClientModel testApp = realm.getClientByClientId("test-app"); + List userSessions = session.sessions().getUserSessions(realm, testApp); + for (UserSessionModel userSession : userSessions) { + persistUserSession(userSession, true); + } + + // Persist 1 online session + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + persistUserSession(userSession, false); + + resetSession(); + + // Assert online session + List loadedSessions = loadPersistedSessionsPaginated(false, 1, 1, 1); + UserSessionProviderTest.assertSession(loadedSessions.get(0), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party"); + + // Assert offline sessions + loadedSessions = loadPersistedSessionsPaginated(true, 2, 2, 3); + UserSessionProviderTest.assertSessions(loadedSessions, origSessions); + + assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party"); + assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started, "test-app"); + assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app"); + } + + @Test + public void testUpdateAndRemove() { + // Create some sessions in infinispan + int started = Time.currentTime(); + UserSessionModel[] origSessions = createSessions(); + + resetSession(); + + // Persist 1 offline session + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[1].getId()); + persistUserSession(userSession, true); + + resetSession(); + + // Load offline session + List loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1); + UserSessionModel persistedSession = loadedSessions.get(0); + UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started, "test-app"); + + // Update userSession + Time.setOffset(10); + try { + persistedSession.setLastSessionRefresh(Time.currentTime()); + persistedSession.setNote("foo", "bar"); + persistedSession.setState(UserSessionModel.State.LOGGING_IN); + persister.updateUserSession(persistedSession, true); + + // create new clientSession + ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()), + "http://redirect", "state", new HashSet(), new HashSet()); + persister.createClientSession(clientSession, true); + + resetSession(); + + // Assert session updated + loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1); + persistedSession = loadedSessions.get(0); + UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app", "third-party"); + Assert.assertEquals("bar", persistedSession.getNote("foo")); + Assert.assertEquals(UserSessionModel.State.LOGGING_IN, persistedSession.getState()); + + // Remove clientSession + persister.removeClientSession(clientSession.getId(), true); + + resetSession(); + + // Assert clientSession removed + loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1); + persistedSession = loadedSessions.get(0); + UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started + 10, "test-app"); + + // Remove userSession + persister.removeUserSession(persistedSession.getId(), true); + + resetSession(); + + // Assert nothing found + loadPersistedSessionsPaginated(true, 10, 0, 0); + } finally { + Time.setOffset(0); + } + } + + @Test + public void testOnRealmRemoved() { + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.addClient("foo-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + + resetSession(); + + // Persist offline session + fooRealm = session.realms().getRealm("foo"); + userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); + persistUserSession(userSession, true); + + resetSession(); + + // Assert session was persisted + loadPersistedSessionsPaginated(true, 10, 1, 1); + + // Remove realm + RealmManager realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + + resetSession(); + + // Assert nothing loaded + loadPersistedSessionsPaginated(true, 10, 0, 0); + } + + @Test + public void testOnClientRemoved() { + int started = Time.currentTime(); + + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.addClient("foo-app"); + fooRealm.addClient("bar-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + + resetSession(); + + // Persist offline session + fooRealm = session.realms().getRealm("foo"); + userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); + persistUserSession(userSession, true); + + resetSession(); + + RealmManager realmMgr = new RealmManager(session); + ClientManager clientMgr = new ClientManager(realmMgr); + fooRealm = realmMgr.getRealm("foo"); + + // Assert session was persisted with both clientSessions + UserSessionModel persistedSession = loadPersistedSessionsPaginated(true, 10, 1, 1).get(0); + UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user3", fooRealm), "127.0.0.1", started, started, "foo-app", "bar-app"); + + // Remove foo-app client + ClientModel client = fooRealm.getClientByClientId("foo-app"); + clientMgr.removeClient(fooRealm, client); + + resetSession(); + + realmMgr = new RealmManager(session); + clientMgr = new ClientManager(realmMgr); + fooRealm = realmMgr.getRealm("foo"); + + // Assert just one bar-app clientSession persisted now + persistedSession = loadPersistedSessionsPaginated(true, 10, 1, 1).get(0); + UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user3", fooRealm), "127.0.0.1", started, started, "bar-app"); + + // Remove bar-app client + client = fooRealm.getClientByClientId("bar-app"); + clientMgr.removeClient(fooRealm, client); + + resetSession(); + + // Assert nothing loaded - userSession was removed as well because it was last userSession + loadPersistedSessionsPaginated(true, 10, 0, 0); + + // Cleanup + realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + } + +// @Test +// public void testExpiredUserSessions() { +// +// } + + + private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + if (userSession != null) clientSession.setUserSession(userSession); + clientSession.setRedirectUri(redirect); + if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); + if (roles != null) clientSession.setRoles(roles); + if (protocolMappers != null) clientSession.setProtocolMappers(protocolMappers); + return clientSession; + } + + private UserSessionModel[] createSessions() { + UserSessionModel[] sessions = new UserSessionModel[3]; + sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + + Set roles = new HashSet(); + roles.add("one"); + roles.add("two"); + + Set protocolMappers = new HashSet(); + protocolMappers.add("mapper-one"); + protocolMappers.add("mapper-two"); + + createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); + createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); + + sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); + + sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); + + return sessions; + } + + private void persistUserSession(UserSessionModel userSession, boolean offline) { + persister.createUserSession(userSession, offline); + for (ClientSessionModel clientSession : userSession.getClientSessions()) { + persister.createClientSession(clientSession, offline); + } + } + + private void resetSession() { + kc.stopSession(session, true); + session = kc.startSession(); + realm = session.realms().getRealm("test"); + persister = session.getProvider(UserSessionPersisterProvider.class); + } + + public static void assertSessionLoaded(List sessions, String id, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) { + for (UserSessionModel session : sessions) { + if (session.getId().equals(id)) { + UserSessionProviderTest.assertSession(session, user, ipAddress, started, lastRefresh, clients); + return; + } + } + Assert.fail("Session with ID " + id + " not found in the list"); + } + + private List loadPersistedSessionsPaginated(boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) { + int count = persister.getUserSessionsCount(offline); + + int start = 0; + int pageCount = 0; + boolean next = true; + List result = new ArrayList<>(); + while (next && start < count) { + List sess = persister.loadUserSessions(start, sessionsPerPage, offline); + if (sess.size() == 0) { + next = false; + } else { + pageCount++; + start += sess.size(); + result.addAll(sess); + } + } + + Assert.assertEquals(pageCount, expectedPageCount); + Assert.assertEquals(count, expectedSessionsCount); + Assert.assertEquals(count, result.size()); + return result; + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java new file mode 100644 index 0000000000..c843928429 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -0,0 +1,348 @@ +package org.keycloak.testsuite.model; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.managers.UserManager; +import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.util.Time; + +/** + * @author Marek Posolda + */ +public class UserSessionProviderOfflineTest { + + @ClassRule + public static KeycloakRule kc = new KeycloakRule(); + + private KeycloakSession session; + private RealmModel realm; + private UserSessionManager sessionManager; + + @Before + public void before() { + session = kc.startSession(); + realm = session.realms().getRealm("test"); + session.users().addUser(realm, "user1").setEmail("user1@localhost"); + session.users().addUser(realm, "user2").setEmail("user2@localhost"); + sessionManager = new UserSessionManager(session); + } + + @After + public void after() { + resetSession(); + session.sessions().removeUserSessions(realm); + UserModel user1 = session.users().getUserByUsername("user1", realm); + UserModel user2 = session.users().getUserByUsername("user2", realm); + + UserManager um = new UserManager(session); + um.removeUser(realm, user1); + um.removeUser(realm, user2); + kc.stopSession(session, true); + } + + + @Test + public void testOfflineSessionsCrud() { + // Create some online sessions in infinispan + int started = Time.currentTime(); + UserSessionModel[] origSessions = createSessions(); + + resetSession(); + + Map offlineSessions = new HashMap<>(); + + // Persist 3 created userSessions and clientSessions as offline + ClientModel testApp = realm.getClientByClientId("test-app"); + List userSessions = session.sessions().getUserSessions(realm, testApp); + for (UserSessionModel userSession : userSessions) { + offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession)); + } + + resetSession(); + + // Assert all previously saved offline sessions found + for (Map.Entry entry : offlineSessions.entrySet()) { + Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null); + + UserSessionModel offlineSession = session.sessions().getUserSession(realm, entry.getValue()); + boolean found = false; + for (ClientSessionModel clientSession : offlineSession.getClientSessions()) { + if (clientSession.getId().equals(entry.getKey())) { + found = true; + } + } + Assert.assertTrue(found); + } + + // Find clients with offline token + UserModel user1 = session.users().getUserByUsername("user1", realm); + Set clients = sessionManager.findClientsWithOfflineToken(realm, user1); + Assert.assertEquals(clients.size(), 2); + for (ClientModel client : clients) { + Assert.assertTrue(client.getClientId().equals("test-app") || client.getClientId().equals("third-party")); + } + + UserModel user2 = session.users().getUserByUsername("user2", realm); + clients = sessionManager.findClientsWithOfflineToken(realm, user2); + Assert.assertEquals(clients.size(), 1); + Assert.assertTrue(clients.iterator().next().getClientId().equals("test-app")); + + // Test count + testApp = realm.getClientByClientId("test-app"); + ClientModel thirdparty = realm.getClientByClientId("third-party"); + Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp)); + Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty)); + + // Revoke "test-app" for user1 + sessionManager.revokeOfflineToken(user1, testApp); + + resetSession(); + + // Assert userSession revoked + testApp = realm.getClientByClientId("test-app"); + thirdparty = realm.getClientByClientId("third-party"); + Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, testApp)); + Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty)); + + List testAppSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10); + List thirdpartySessions = session.sessions().getOfflineUserSessions(realm, thirdparty, 0, 10); + Assert.assertEquals(1, testAppSessions.size()); + Assert.assertEquals("127.0.0.3", testAppSessions.get(0).getIpAddress()); + Assert.assertEquals("user2", testAppSessions.get(0).getUser().getUsername()); + Assert.assertEquals(1, thirdpartySessions.size()); + Assert.assertEquals("127.0.0.1", thirdpartySessions.get(0).getIpAddress()); + Assert.assertEquals("user1", thirdpartySessions.get(0).getUser().getUsername()); + + user1 = session.users().getUserByUsername("user1", realm); + user2 = session.users().getUserByUsername("user2", realm); + clients = sessionManager.findClientsWithOfflineToken(realm, user1); + Assert.assertEquals(1, clients.size()); + Assert.assertEquals("third-party", clients.iterator().next().getClientId()); + clients = sessionManager.findClientsWithOfflineToken(realm, user2); + Assert.assertEquals(1, clients.size()); + Assert.assertEquals("test-app", clients.iterator().next().getClientId()); + } + + @Test + public void testOnRealmRemoved() { + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.addClient("foo-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + + resetSession(); + + // Persist offline session + fooRealm = session.realms().getRealm("foo"); + userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); + clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId()); + sessionManager.persistOfflineSession(userSession.getClientSessions().get(0), userSession); + + resetSession(); + + ClientSessionModel offlineClientSession = sessionManager.findOfflineClientSession(fooRealm, clientSession.getId(), userSession.getId()); + Assert.assertEquals("foo-app", offlineClientSession.getClient().getClientId()); + Assert.assertEquals("user3", offlineClientSession.getUserSession().getUser().getUsername()); + Assert.assertEquals(offlineClientSession.getId(), offlineClientSession.getUserSession().getClientSessions().get(0).getId()); + + // Remove realm + RealmManager realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + + resetSession(); + + fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.addClient("foo-app"); + session.users().addUser(fooRealm, "user3"); + + resetSession(); + + // Assert nothing loaded + fooRealm = session.realms().getRealm("foo"); + Assert.assertNull(sessionManager.findOfflineClientSession(fooRealm, clientSession.getId(), userSession.getId())); + Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(fooRealm, fooRealm.getClientByClientId("foo-app"))); + + // Cleanup + realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + } + + @Test + public void testOnClientRemoved() { + int started = Time.currentTime(); + + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.addClient("foo-app"); + fooRealm.addClient("bar-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + + resetSession(); + + // Create offline session + fooRealm = session.realms().getRealm("foo"); + userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); + createOfflineSessionIncludeClientSessions(userSession); + + resetSession(); + + RealmManager realmMgr = new RealmManager(session); + ClientManager clientMgr = new ClientManager(realmMgr); + fooRealm = realmMgr.getRealm("foo"); + + // Assert session was persisted with both clientSessions + UserSessionModel offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId()); + UserSessionProviderTest.assertSession(offlineSession, session.users().getUserByUsername("user3", fooRealm), "127.0.0.1", started, started, "foo-app", "bar-app"); + + // Remove foo-app client + ClientModel client = fooRealm.getClientByClientId("foo-app"); + clientMgr.removeClient(fooRealm, client); + + resetSession(); + + realmMgr = new RealmManager(session); + clientMgr = new ClientManager(realmMgr); + fooRealm = realmMgr.getRealm("foo"); + + // Assert just one bar-app clientSession persisted now + offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId()); + Assert.assertEquals(1, offlineSession.getClientSessions().size()); + Assert.assertEquals("bar-app", offlineSession.getClientSessions().get(0).getClient().getClientId()); + + // Remove bar-app client + client = fooRealm.getClientByClientId("bar-app"); + clientMgr.removeClient(fooRealm, client); + + resetSession(); + + // Assert nothing loaded - userSession was removed as well because it was last userSession + offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId()); + Assert.assertEquals(0, offlineSession.getClientSessions().size()); + + // Cleanup + realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + } + + @Test + public void testOnUserRemoved() { + int started = Time.currentTime(); + + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.addClient("foo-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + + resetSession(); + + // Create offline session + fooRealm = session.realms().getRealm("foo"); + userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); + createOfflineSessionIncludeClientSessions(userSession); + + resetSession(); + + RealmManager realmMgr = new RealmManager(session); + fooRealm = realmMgr.getRealm("foo"); + UserModel user3 = session.users().getUserByUsername("user3", fooRealm); + + // Assert session was persisted with both clientSessions + UserSessionModel offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId()); + UserSessionProviderTest.assertSession(offlineSession, user3, "127.0.0.1", started, started, "foo-app"); + + // Remove user3 + new UserManager(session).removeUser(fooRealm, user3); + + resetSession(); + + // Assert userSession removed as well + Assert.assertNull(session.sessions().getOfflineUserSession(fooRealm, userSession.getId())); + Assert.assertNull(session.sessions().getOfflineClientSession(fooRealm, clientSession.getId())); + + // Cleanup + realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + + } + + private Map createOfflineSessionIncludeClientSessions(UserSessionModel userSession) { + Map offlineSessions = new HashMap<>(); + + UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(userSession); + for (ClientSessionModel clientSession : userSession.getClientSessions()) { + ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession); + offlineClientSession.setUserSession(offlineUserSession); + offlineSessions.put(clientSession.getId(), userSession.getId()); + } + return offlineSessions; + } + + + + private void resetSession() { + kc.stopSession(session, true); + session = kc.startSession(); + realm = session.realms().getRealm("test"); + sessionManager = new UserSessionManager(session); + } + + private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + ClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client); + if (userSession != null) clientSession.setUserSession(userSession); + clientSession.setRedirectUri(redirect); + if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); + if (roles != null) clientSession.setRoles(roles); + if (protocolMappers != null) clientSession.setProtocolMappers(protocolMappers); + return clientSession; + } + + private UserSessionModel[] createSessions() { + UserSessionModel[] sessions = new UserSessionModel[3]; + sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + + Set roles = new HashSet(); + roles.add("one"); + roles.add("two"); + + Set protocolMappers = new HashSet(); + protocolMappers.add("mapper-one"); + protocolMappers.add("mapper-two"); + + createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); + createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); + + sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); + + sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); + + return sessions; + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index 7ca33e2148..0b099dede4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.model; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -400,6 +401,19 @@ public class UserSessionProviderTest { assertPaginatedSession(realm, realm.getClientByClientId("test-app"), 30, 10, 0); } + @Test + public void testCreateAndGetInSameTransaction() { + UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("test-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + + Assert.assertNotNull(session.sessions().getUserSession(realm, userSession.getId())); + Assert.assertNotNull(session.sessions().getClientSession(realm, clientSession.getId())); + + Assert.assertEquals(userSession.getId(), clientSession.getUserSession().getId()); + Assert.assertEquals(1, userSession.getClientSessions().size()); + Assert.assertEquals(clientSession.getId(), userSession.getClientSessions().get(0).getId()); + } + private void assertPaginatedSession(RealmModel realm, ClientModel client, int start, int max, int expectedSize) { List sessions = session.sessions().getUserSessions(realm, client, start, max); String[] actualIps = new String[sessions.size()]; @@ -515,7 +529,7 @@ public class UserSessionProviderTest { realm = session.realms().getRealm("test"); } - public void assertSessions(List actualSessions, UserSessionModel... expectedSessions) { + public static void assertSessions(List actualSessions, UserSessionModel... expectedSessions) { String[] expected = new String[expectedSessions.length]; for (int i = 0; i < expected.length; i++) { expected[i] = expectedSessions[i].getId(); @@ -532,7 +546,7 @@ public class UserSessionProviderTest { assertArrayEquals(expected, actual); } - public void assertSession(UserSessionModel session, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) { + public static void assertSession(UserSessionModel session, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) { assertEquals(user.getId(), session.getUser().getId()); assertEquals(ipAddress, session.getIpAddress()); assertEquals(user.getUsername(), session.getLoginUsername()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index a8c3d9c531..9f95f2617d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -343,7 +343,7 @@ public class OfflineTokenTest { testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId); - // Now retrieve another offline token and verify that previous offline token is not valid anymore + // Now retrieve another offline token and verify that previous offline token is still valid tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1"); AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken()); @@ -360,21 +360,8 @@ public class OfflineTokenTest { .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client") .assertEvent(); - // Refresh with old offline token should fail - OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1"); - Assert.assertEquals(400, response.getStatusCode()); - Assert.assertEquals("invalid_grant", response.getError()); - - events.expectRefresh(offlineToken.getId(), offlineToken.getSessionState()) - .error(Errors.INVALID_TOKEN) - .client("offline-client") - .user(serviceAccountUserId) - .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID) - .removeDetail(Details.TOKEN_ID) - .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) - .assertEvent(); - - // Refresh with new offline token is ok + // Refresh with both offline tokens is fine + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId); testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId); } diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index cd56ce0511..26fec935b5 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -22,6 +22,16 @@ "provider": "${keycloak.user.provider:jpa}" }, + "userSessions": { + "infinispan": { + "enforceCompat": "${keycloak.connectionsInfinispan.enforceCompat:false}" + } + }, + + "userSessionPersister": { + "provider": "${keycloak.userSessionPersister.provider:jpa}" + }, + "timer": { "provider": "basic" }, @@ -68,5 +78,13 @@ "databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}", "connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}" } + }, + + "connectionsInfinispan": { + "default": { + "clustered": "${keycloak.connectionsInfinispan.clustered:false}", + "async": "${keycloak.connectionsInfinispan.async:true}", + "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}" + } } } \ No newline at end of file