From 515bfb5064fc2552e521c302507a29b9962f1d81 Mon Sep 17 00:00:00 2001 From: Martin Kanis Date: Fri, 29 Jan 2021 13:04:35 +0100 Subject: [PATCH] KEYCLOAK-16378 User / client session map store Co-authored-by: Martin Kanis Co-authored-by: Hynek Mlnarik --- .github/workflows/ci.yml | 2 +- .../org/keycloak/common/util/StackUtil.java | 2 +- core/src/main/java/org/keycloak/Config.java | 6 +- .../AuthenticatedClientSessionAdapter.java | 2 +- .../InfinispanUserLoginFailureProvider.java | 157 ++++ ...nispanUserLoginFailureProviderFactory.java | 214 ++++++ .../InfinispanUserSessionProvider.java | 96 +-- .../InfinispanUserSessionProviderFactory.java | 51 +- .../infinispan/UserLoginFailureAdapter.java | 4 +- .../AbstractUserSessionClusterListener.java | 15 +- ...oak.models.UserLoginFailureProviderFactory | 18 + .../AbstractUserLoginFailureEntity.java | 129 ++++ .../AbstractUserLoginFailureModel.java | 56 ++ .../MapUserLoginFailureAdapter.java | 84 +++ .../MapUserLoginFailureEntity.java | 32 + .../MapUserLoginFailureProvider.java | 121 ++++ .../MapUserLoginFailureProviderFactory.java | 68 ++ .../models/map/realm/MapRealmAdapter.java | 5 + .../map/storage/MapFieldPredicates.java | 39 + .../map/storage/MapKeycloakTransaction.java | 2 +- .../chm/ConcurrentHashMapStorageProvider.java | 13 +- .../UserSessionConcurrentHashMapStorage.java | 79 ++ .../models/map/user/MapUserAdapter.java | 5 + ...tractAuthenticatedClientSessionEntity.java | 205 ++++++ ...stractAuthenticatedClientSessionModel.java | 65 ++ .../AbstractUserSessionEntity.java | 288 ++++++++ .../userSession/AbstractUserSessionModel.java | 56 ++ .../MapAuthenticatedClientSessionAdapter.java | 148 ++++ .../MapAuthenticatedClientSessionEntity.java | 33 + .../userSession/MapUserSessionAdapter.java | 223 ++++++ .../map/userSession/MapUserSessionEntity.java | 41 ++ .../userSession/MapUserSessionProvider.java | 680 ++++++++++++++++++ .../MapUserSessionProviderFactory.java | 66 ++ .../map/userSession/SessionExpiration.java | 153 ++++ ...oak.models.UserLoginFailureProviderFactory | 18 + ...keycloak.models.UserSessionProviderFactory | 18 + .../UserLoginFailureProviderFactory.java | 26 + .../keycloak/models/UserLoginFailureSpi.java | 49 ++ .../services/org.keycloak.provider.Spi | 1 + .../AuthenticatedClientSessionModel.java | 10 + .../org/keycloak/models/KeycloakSession.java | 9 +- .../models/UserLoginFailureModel.java | 12 +- .../models/UserLoginFailureProvider.java | 55 ++ .../org/keycloak/models/UserSessionModel.java | 24 + .../keycloak/models/UserSessionProvider.java | 57 +- .../keycloak/protocol/oidc/TokenManager.java | 9 +- .../services/DefaultKeycloakSession.java | 11 +- .../managers/AuthenticationManager.java | 8 +- .../managers/DefaultBruteForceProtector.java | 6 +- .../admin/AttackDetectionResource.java | 8 +- .../resources/admin/UserResource.java | 2 +- .../servlet/AbstractShowTokensServlet.java | 1 + .../adapter/page/AbstractShowTokensPage.java | 12 + .../testsuite/adapter/page/OfflineToken.java | 5 +- .../servlet/OfflineServletsAdapterTest.java | 28 +- .../concurrency/ConcurrentLoginTest.java | 15 + .../crossdc/BruteForceCrossDCTest.java | 2 +- .../federation/storage/ClientStorageTest.java | 6 +- .../keycloak/testsuite/forms/LogoutTest.java | 10 +- .../keycloak/testsuite/model/CacheTest.java | 3 +- .../model/UserSessionInitializerTest.java | 278 ------- .../UserSessionPersisterProviderTest.java | 613 ---------------- .../model/UserSessionProviderOfflineTest.java | 171 +---- .../model/UserSessionProviderTest.java | 240 +++---- .../oauth/BackchannelLogoutTest.java | 26 +- .../testsuite/oauth/OfflineTokenTest.java | 50 +- .../resources/META-INF/keycloak-server.json | 12 +- .../offline-client/offlinerealm.json | 5 +- .../base/src/test/resources/testrealm.json | 4 + .../testsuite/model/ResteasyNullProvider.java | 45 ++ .../org.keycloak.common.util.ResteasyProvider | 17 + .../testsuite/model/KeycloakModelTest.java | 11 +- .../model/UserSessionInitializerTest.java | 217 ++++++ .../UserSessionPersisterProviderTest.java | 625 ++++++++++++++++ .../model/UserSessionProviderModelTest.java | 197 +++++ .../UserSessionProviderOfflineModelTest.java | 234 ++++++ .../model/parameters/Infinispan.java | 13 + .../testsuite/model/parameters/Jpa.java | 4 + .../testsuite/model/parameters/Map.java | 11 + .../model/src/test/resources/log4j.properties | 7 +- .../resources/META-INF/keycloak-server.json | 8 + 81 files changed, 4945 insertions(+), 1406 deletions(-) create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProvider.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProviderFactory.java create mode 100644 model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.UserLoginFailureProviderFactory create mode 100644 model/map/src/main/java/org/keycloak/models/map/loginFailure/AbstractUserLoginFailureEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/loginFailure/AbstractUserLoginFailureModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProvider.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/userSession/SessionExpiration.java create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.models.UserLoginFailureProviderFactory create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory create mode 100644 server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureSpi.java create mode 100644 server-spi/src/main/java/org/keycloak/models/UserLoginFailureProvider.java delete mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java delete mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java create mode 100644 testsuite/model/src/main/java/org/keycloak/testsuite/model/ResteasyNullProvider.java create mode 100644 testsuite/model/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider create mode 100644 testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java create mode 100644 testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java create mode 100644 testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderModelTest.java create mode 100644 testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineModelTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 312e7bd7f0..aae3a3da41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,7 +160,7 @@ jobs: run: | declare -A PARAMS TESTGROUP PARAMS["quarkus"]="-Pauth-server-quarkus" - PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map -Dkeycloak.realm.provider=map -Dkeycloak.authorization.provider=map" + PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.userSession.provider=map -Dkeycloak.loginFailure.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map -Dkeycloak.realm.provider=map -Dkeycloak.authorization.provider=map" PARAMS["wildfly"]="-Pauth-server-wildfly" TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r" TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b" diff --git a/common/src/main/java/org/keycloak/common/util/StackUtil.java b/common/src/main/java/org/keycloak/common/util/StackUtil.java index 9096feebe5..bda80e58ff 100644 --- a/common/src/main/java/org/keycloak/common/util/StackUtil.java +++ b/common/src/main/java/org/keycloak/common/util/StackUtil.java @@ -31,7 +31,7 @@ public class StackUtil { return getShortStackTrace("\n "); } - private static final Pattern IGNORED = Pattern.compile("sun\\.|java\\.(lang|util|stream)\\.|org\\.jboss\\.(arquillian|logging).|org.apache.maven.surefire"); + private static final Pattern IGNORED = Pattern.compile("sun\\.|java\\.(lang|util|stream)\\.|org\\.jboss\\.(arquillian|logging).|org.apache.maven.surefire|org\\.junit\\.|org.keycloak.testsuite.model.KeycloakModelTest\\."); private static final StringBuilder EMPTY = new StringBuilder(0); /** diff --git a/core/src/main/java/org/keycloak/Config.java b/core/src/main/java/org/keycloak/Config.java index 111b42ddb4..8bee11bd55 100755 --- a/core/src/main/java/org/keycloak/Config.java +++ b/core/src/main/java/org/keycloak/Config.java @@ -114,7 +114,7 @@ public class Config { @Override public Integer getInt(String key, Integer defaultValue) { String v = get(key, null); - return v != null ? Integer.parseInt(v) : defaultValue; + return v != null ? Integer.valueOf(v) : defaultValue; } @Override @@ -125,7 +125,7 @@ public class Config { @Override public Long getLong(String key, Long defaultValue) { String v = get(key, null); - return v != null ? Long.parseLong(v) : defaultValue; + return v != null ? Long.valueOf(v) : defaultValue; } @Override @@ -137,7 +137,7 @@ public class Config { public Boolean getBoolean(String key, Boolean defaultValue) { String v = get(key, null); if (v != null) { - return Boolean.parseBoolean(v); + return Boolean.valueOf(v); } else { return defaultValue; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java index 729aa01764..4e1ed8bee3 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java @@ -117,7 +117,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public String getId() { - return null; + return entity.getId().toString(); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProvider.java new file mode 100644 index 0000000000..ce0b065f9f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProvider.java @@ -0,0 +1,157 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserLoginFailureProvider; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserLoginFailureModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.Tasks; +import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; +import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; +import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; +import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; +import org.keycloak.models.sessions.infinispan.stream.Mappers; +import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate; +import org.keycloak.models.sessions.infinispan.util.FuturesHelper; +import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; + +import java.util.concurrent.Future; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; + +/** + * + * @author Martin Kanis + */ +public class InfinispanUserLoginFailureProvider implements UserLoginFailureProvider { + + private static final Logger log = Logger.getLogger(InfinispanUserLoginFailureProvider.class); + + protected final KeycloakSession session; + + + protected final Cache> loginFailureCache; + protected final InfinispanChangelogBasedTransaction loginFailuresTx; + protected final SessionEventsSenderTransaction clusterEventsSenderTx; + + public InfinispanUserLoginFailureProvider(KeycloakSession session, + RemoteCacheInvoker remoteCacheInvoker, + Cache> loginFailureCache) { + this.session = session; + this.loginFailureCache = loginFailureCache; + this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs); + this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); + + session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); + session.getTransactionManager().enlistAfterCompletion(loginFailuresTx); + } + + + @Override + public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) { + log.tracef("getUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace()); + + LoginFailureKey key = new LoginFailureKey(realm.getId(), userId); + LoginFailureEntity entity = getLoginFailureEntity(key); + return wrap(key, entity); + } + + @Override + public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) { + log.tracef("addUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace()); + + LoginFailureKey key = new LoginFailureKey(realm.getId(), userId); + LoginFailureEntity entity = new LoginFailureEntity(); + entity.setRealmId(realm.getId()); + entity.setUserId(userId); + + SessionUpdateTask createLoginFailureTask = Tasks.addIfAbsentSync(); + loginFailuresTx.addTask(key, createLoginFailureTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); + + return wrap(key, entity); + } + + @Override + public void removeUserLoginFailure(RealmModel realm, String userId) { + log.tracef("removeUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace()); + + SessionUpdateTask removeTask = Tasks.removeSync(); + loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask); + } + + @Override + public void removeAllUserLoginFailures(RealmModel realm) { + log.tracef("removeAllUserLoginFailures(%s)%s", realm, getShortStackTrace()); + + clusterEventsSenderTx.addEvent( + RemoveAllUserLoginFailuresEvent.createEvent(RemoveAllUserLoginFailuresEvent.class, InfinispanUserLoginFailureProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, session, realm.getId(), true), + ClusterProvider.DCNotify.LOCAL_DC_ONLY); + } + + protected void removeAllLocalUserLoginFailuresEvent(String realmId) { + log.tracef("removeAllLocalUserLoginFailuresEvent(%s)%s", realmId, getShortStackTrace()); + + FuturesHelper futures = new FuturesHelper(); + + Cache> localCache = CacheDecorators.localCache(loginFailureCache); + + Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + + localCacheStoreIgnore + .entrySet() + .stream() + .filter(UserLoginFailurePredicate.create(realmId)) + .map(Mappers.loginFailureId()) + .forEach(loginFailureKey -> { + // Remove loginFailure from remoteCache too. Use removeAsync for better perf + Future future = localCache.removeAsync(loginFailureKey); + futures.addTask(future); + }); + + futures.waitForAllToFinish(); + + log.debugf("Removed %d login failures in realm %s", futures.size(), realmId); + } + + UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { + return entity != null ? new UserLoginFailureAdapter(this, key, entity) : null; + } + + private LoginFailureEntity getLoginFailureEntity(LoginFailureKey key) { + InfinispanChangelogBasedTransaction tx = getLoginFailuresTx(); + SessionEntityWrapper entityWrapper = tx.get(key); + return entityWrapper==null ? null : entityWrapper.getEntity(); + } + + InfinispanChangelogBasedTransaction getLoginFailuresTx() { + return loginFailuresTx; + } + + @Override + public void close() { + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProviderFactory.java new file mode 100644 index 0000000000..600c93903c --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserLoginFailureProviderFactory.java @@ -0,0 +1,214 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.persistence.remote.RemoteStore; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.util.Time; +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.UserLoginFailureProvider; +import org.keycloak.models.UserLoginFailureProviderFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +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.events.AbstractUserSessionClusterListener; +import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; +import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionListener; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.PostMigrationEvent; + +import java.io.Serializable; +import java.util.Set; +import java.util.function.BiFunction; + +/** + * @author Martin Kanis + */ +public class InfinispanUserLoginFailureProviderFactory implements UserLoginFailureProviderFactory { + + private static final Logger log = Logger.getLogger(InfinispanUserLoginFailureProviderFactory.class); + + public static final String PROVIDER_ID = "infinispan"; + + public static final String REALM_REMOVED_SESSION_EVENT = "REALM_REMOVED_EVENT_SESSIONS"; + + public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT"; + + private Config.Scope config; + + private RemoteCacheInvoker remoteCacheInvoker; + + @Override + public UserLoginFailureProvider create(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); + + return new InfinispanUserLoginFailureProvider(session, remoteCacheInvoker, loginFailures); + } + + @Override + public void init(Config.Scope config) { + this.config = config; + } + + @Override + public void postInit(final KeycloakSessionFactory factory) { + this.remoteCacheInvoker = new RemoteCacheInvoker(); + + factory.register(event -> { + if (event instanceof PostMigrationEvent) { + KeycloakModelUtils.runJobInTransaction(factory, (KeycloakSession session) -> { + checkRemoteCaches(session); + registerClusterListeners(session); + loadLoginFailuresFromRemoteCaches(session); + }); + } else if (event instanceof UserModel.UserRemovedEvent) { + UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; + + UserLoginFailureProvider provider = userRemovedEvent.getKeycloakSession().getProvider(UserLoginFailureProvider.class, getId()); + provider.removeUserLoginFailure(userRemovedEvent.getRealm(), userRemovedEvent.getUser().getId()); + } + }); + } + + protected void registerClusterListeners(KeycloakSession session) { + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + + cluster.registerListener(REALM_REMOVED_SESSION_EVENT, + new AbstractUserSessionClusterListener(sessionFactory, UserLoginFailureProvider.class) { + + @Override + protected void eventReceived(KeycloakSession session, UserLoginFailureProvider provider, RealmRemovedSessionEvent sessionEvent) { + if (provider instanceof InfinispanUserLoginFailureProvider) { + ((InfinispanUserLoginFailureProvider) provider).removeAllLocalUserLoginFailuresEvent(sessionEvent.getRealmId()); + } + } + }); + + cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, + new AbstractUserSessionClusterListener(sessionFactory, UserLoginFailureProvider.class) { + + @Override + protected void eventReceived(KeycloakSession session, UserLoginFailureProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) { + if (provider instanceof InfinispanUserLoginFailureProvider) { + ((InfinispanUserLoginFailureProvider) provider).removeAllLocalUserLoginFailuresEvent(sessionEvent.getRealmId()); + } + } + + }); + + log.debug("Registered cluster listeners"); + } + + + protected void checkRemoteCaches(KeycloakSession session) { + InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class); + + Cache> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); + checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> + Time.toMillis(realm.getMaxDeltaTimeSeconds()), SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs); + } + + private RemoteCache checkRemoteCache(KeycloakSession session, Cache> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader, + BiFunction lifespanMsLoader, BiFunction maxIdleTimeMsLoader) { + Set remoteStores = InfinispanUtil.getRemoteStores(ispnCache); + + if (remoteStores.isEmpty()) { + log.debugf("No remote store configured for cache '%s'", ispnCache.getName()); + return null; + } else { + log.infof("Remote store configured for cache '%s'", ispnCache.getName()); + + RemoteCache> remoteCache = (RemoteCache) remoteStores.iterator().next().getRemoteCache(); + + if (remoteCache == null) { + throw new IllegalStateException("No remote cache available for the infinispan cache: " + ispnCache.getName()); + } + + remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader); + + RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader); + remoteCache.addClientListener(hotrodListener); + return remoteCache; + } + } + + // Max count of worker errors. Initialization will end with exception when this number is reached + private int getMaxErrors() { + return config.getInt("maxErrors", 20); + } + + // Count of sessions to be computed in each segment + private int getSessionsPerSegment() { + return config.getInt("sessionsPerSegment", 64); + } + + private void loadLoginFailuresFromRemoteCaches(KeycloakSession session) { + for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) { + loadLoginFailuresFromRemoteCaches(session.getKeycloakSessionFactory(), cacheName, getSessionsPerSegment(), getMaxErrors()); + } + } + + private void loadLoginFailuresFromRemoteCaches(final KeycloakSessionFactory sessionFactory, String cacheName, final int sessionsPerSegment, final int maxErrors) { + log.debugf("Check pre-loading sessions from remote cache '%s'", cacheName); + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); + + InfinispanCacheInitializer initializer = new InfinispanCacheInitializer(sessionFactory, workCache, + new RemoteCacheSessionsLoader(cacheName, sessionsPerSegment), "remoteCacheLoad::" + cacheName, sessionsPerSegment, maxErrors); + + initializer.initCache(); + initializer.loadSessions(); + } + + }); + + log.debugf("Pre-loading login failures from remote cache '%s' finished", cacheName); + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 196e1fea6e..4ac224ee85 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -32,7 +32,6 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; @@ -46,17 +45,13 @@ import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedT import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; -import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; import org.keycloak.models.sessions.infinispan.stream.Comparators; import org.keycloak.models.sessions.infinispan.stream.Mappers; import org.keycloak.models.sessions.infinispan.stream.SessionPredicate; -import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate; import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate; import org.keycloak.models.sessions.infinispan.util.FuturesHelper; import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; @@ -95,13 +90,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected final Cache> offlineSessionCache; protected final Cache> clientSessionCache; protected final Cache> offlineClientSessionCache; - protected final Cache> loginFailureCache; protected final InfinispanChangelogBasedTransaction sessionTx; protected final InfinispanChangelogBasedTransaction offlineSessionTx; protected final InfinispanChangelogBasedTransaction clientSessionTx; protected final InfinispanChangelogBasedTransaction offlineClientSessionTx; - protected final InfinispanChangelogBasedTransaction loginFailuresTx; protected final SessionEventsSenderTransaction clusterEventsSenderTx; @@ -121,23 +114,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { Cache> sessionCache, Cache> offlineSessionCache, Cache> clientSessionCache, - Cache> offlineClientSessionCache, - Cache> loginFailureCache) { + Cache> offlineClientSessionCache) { this.session = session; this.sessionCache = sessionCache; this.clientSessionCache = clientSessionCache; this.offlineSessionCache = offlineSessionCache; this.offlineClientSessionCache = offlineClientSessionCache; - this.loginFailureCache = loginFailureCache; this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs); this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs); this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs); this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs); - this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs); - this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); this.lastSessionRefreshStore = lastSessionRefreshStore; @@ -151,7 +140,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { session.getTransactionManager().enlistAfterCompletion(offlineSessionTx); session.getTransactionManager().enlistAfterCompletion(clientSessionTx); session.getTransactionManager().enlistAfterCompletion(offlineClientSessionTx); - session.getTransactionManager().enlistAfterCompletion(loginFailuresTx); } protected Cache> getCache(boolean offline) { @@ -182,6 +170,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return persisterLastSessionRefreshStore; } + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + @Override public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { final UUID clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache); @@ -528,72 +521,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) userSessionsSize.get(), realmId, offline); } - @Override - public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) { - LoginFailureKey key = new LoginFailureKey(realm.getId(), userId); - LoginFailureEntity entity = getLoginFailureEntity(key); - return wrap(key, entity); - } - - private LoginFailureEntity getLoginFailureEntity(LoginFailureKey key) { - InfinispanChangelogBasedTransaction tx = getLoginFailuresTx(); - SessionEntityWrapper entityWrapper = tx.get(key); - return entityWrapper==null ? null : entityWrapper.getEntity(); - } - - @Override - public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) { - LoginFailureKey key = new LoginFailureKey(realm.getId(), userId); - LoginFailureEntity entity = new LoginFailureEntity(); - entity.setRealmId(realm.getId()); - entity.setUserId(userId); - - SessionUpdateTask createLoginFailureTask = Tasks.addIfAbsentSync(); - loginFailuresTx.addTask(key, createLoginFailureTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); - - return wrap(key, entity); - } - - @Override - public void removeUserLoginFailure(RealmModel realm, String userId) { - SessionUpdateTask removeTask = Tasks.removeSync(); - loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask); - } - - @Override - public void removeAllUserLoginFailures(RealmModel realm) { - clusterEventsSenderTx.addEvent( - RemoveAllUserLoginFailuresEvent.createEvent(RemoveAllUserLoginFailuresEvent.class, InfinispanUserSessionProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, session, realm.getId(), true), - ClusterProvider.DCNotify.LOCAL_DC_ONLY); - } - - protected void onRemoveAllUserLoginFailuresEvent(String realmId) { - removeAllLocalUserLoginFailuresEvent(realmId); - } - - private void removeAllLocalUserLoginFailuresEvent(String realmId) { - FuturesHelper futures = new FuturesHelper(); - - Cache> localCache = CacheDecorators.localCache(loginFailureCache); - - Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); - - localCacheStoreIgnore - .entrySet() - .stream() - .filter(UserLoginFailurePredicate.create(realmId)) - .map(Mappers.loginFailureId()) - .forEach(loginFailureKey -> { - // Remove loginFailure from remoteCache too. Use removeAsync for better perf - Future future = localCache.removeAsync(loginFailureKey); - futures.addTask(future); - }); - - futures.waitForAllToFinish(); - - log.debugf("Removed %d login failures in realm %s", futures.size(), realmId); - } - @Override public void onRealmRemoved(RealmModel realm) { // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions. @@ -610,7 +537,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected void onRealmRemovedEvent(String realmId) { removeLocalUserSessions(realmId, true); removeLocalUserSessions(realmId, false); - removeAllLocalUserLoginFailuresEvent(realmId); } @Override @@ -633,8 +559,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { removeUserSessions(realm, user, true); removeUserSessions(realm, user, false); - removeUserLoginFailure(realm, user.getId()); - UserSessionPersisterProvider persisterProvider = session.getProvider(UserSessionPersisterProvider.class); if (persisterProvider != null) { persisterProvider.onUserRemoved(realm, user); @@ -653,10 +577,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask); } - InfinispanChangelogBasedTransaction getLoginFailuresTx() { - return loginFailuresTx; - } - UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); @@ -669,10 +589,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, clientSessionUpdateTx, offline) : null; } - UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { - return entity != null ? new UserLoginFailureAdapter(this, key, entity) : null; - } - UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) { if (userSession instanceof UserSessionAdapter) { if (!userSession.getRealm().equals(realm)) return null; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index 97078520ab..dff8f254a5 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -42,14 +42,11 @@ import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheIniti import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener; import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; -import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer; import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader; @@ -81,8 +78,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider public static final String REMOVE_USER_SESSIONS_EVENT = "REMOVE_USER_SESSIONS_EVENT"; - public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT"; - private Config.Scope config; private RemoteCacheInvoker remoteCacheInvoker; @@ -98,11 +93,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider Cache> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); Cache> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); Cache> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); - Cache> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, - persisterLastSessionRefreshStore, keyGenerator, - cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, loginFailures); + persisterLastSessionRefreshStore, keyGenerator, cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache); } @Override @@ -112,7 +105,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider @Override public void postInit(final KeycloakSessionFactory factory) { - factory.register(new ProviderEventListener() { @Override @@ -202,38 +194,38 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.registerListener(REALM_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + cluster.registerListener(REALM_REMOVED_SESSION_EVENT, + new AbstractUserSessionClusterListener(sessionFactory, UserSessionProvider.class) { @Override - protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) { - provider.onRealmRemovedEvent(sessionEvent.getRealmId()); + protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) { + if (provider instanceof InfinispanUserSessionProvider) { + ((InfinispanUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId()); + } } }); - cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, + new AbstractUserSessionClusterListener(sessionFactory, UserSessionProvider.class) { @Override - protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) { - provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid()); + protected void eventReceived(KeycloakSession session, UserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) { + if (provider instanceof InfinispanUserSessionProvider) { + ((InfinispanUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid()); + } } }); - cluster.registerListener(REMOVE_USER_SESSIONS_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + cluster.registerListener(REMOVE_USER_SESSIONS_EVENT, + new AbstractUserSessionClusterListener(sessionFactory, UserSessionProvider.class) { @Override - protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) { - provider.onRemoveUserSessionsEvent(sessionEvent.getRealmId()); - } - - }); - - cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { - - @Override - protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) { - provider.onRemoveAllUserLoginFailuresEvent(sessionEvent.getRealmId()); + protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) { + if (provider instanceof InfinispanUserSessionProvider) { + ((InfinispanUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId()); + } } }); @@ -276,11 +268,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> { return Time.toMillis(realm.getOfflineSessionIdleTimeout()); }, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs); - - Cache> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); - checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> { - return Time.toMillis(realm.getMaxDeltaTimeSeconds()); - }, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs); } private RemoteCache checkRemoteCache(KeycloakSession session, Cache> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader, diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java index 0971c26212..658de000af 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java @@ -27,11 +27,11 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; */ public class UserLoginFailureAdapter implements UserLoginFailureModel { - private InfinispanUserSessionProvider provider; + private InfinispanUserLoginFailureProvider provider; private LoginFailureKey key; private LoginFailureEntity entity; - public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, LoginFailureKey key, LoginFailureEntity entity) { + public UserLoginFailureAdapter(InfinispanUserLoginFailureProvider provider, LoginFailureKey key, LoginFailureEntity entity) { this.provider = provider; this.key = key; this.entity = entity; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java index 1c83f0cf1e..a4decf5d4c 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java @@ -24,30 +24,31 @@ import org.keycloak.cluster.ClusterProvider; import org.keycloak.connections.infinispan.TopologyInfo; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.UserSessionProvider; -import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider; -import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.Provider; /** * @author Marek Posolda */ -public abstract class AbstractUserSessionClusterListener implements ClusterListener { +public abstract class AbstractUserSessionClusterListener implements ClusterListener { private static final Logger log = Logger.getLogger(AbstractUserSessionClusterListener.class); private final KeycloakSessionFactory sessionFactory; - public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory) { + private final Class providerClazz; + + public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory, Class providerClazz) { this.sessionFactory = sessionFactory; + this.providerClazz = providerClazz; } @Override public void eventReceived(ClusterEvent event) { KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> { - InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) session.getProvider(UserSessionProvider.class, InfinispanUserSessionProviderFactory.PROVIDER_ID); + T provider = session.getProvider(providerClazz); SE sessionEvent = (SE) event; boolean shouldResendEvent = shouldResendEvent(session, sessionEvent); @@ -65,7 +66,7 @@ public abstract class AbstractUserSessionClusterListenerMartin Kanis + */ +public abstract class AbstractUserLoginFailureEntity implements AbstractEntity { + private K id; + private String realmId; + private String userId; + + /** + * Flag signalizing that any of the setters has been meaningfully used. + */ + protected boolean updated; + + private int failedLoginNotBefore; + private int numFailures; + private long lastFailure; + private String lastIPFailure; + + public AbstractUserLoginFailureEntity() { + this.id = null; + this.realmId = null; + this.userId = null; + } + + public AbstractUserLoginFailureEntity(K id, String realmId, String userId) { + this.id = id; + this.realmId = realmId; + this.userId = userId; + } + + @Override + public K getId() { + return this.id; + } + + @Override + public boolean isUpdated() { + return this.updated; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.updated |= !Objects.equals(this.realmId, realmId); + this.realmId = realmId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.updated |= !Objects.equals(this.userId, userId); + this.userId = userId; + } + + public int getFailedLoginNotBefore() { + return failedLoginNotBefore; + } + + public void setFailedLoginNotBefore(int failedLoginNotBefore) { + this.updated |= this.failedLoginNotBefore != failedLoginNotBefore; + this.failedLoginNotBefore = failedLoginNotBefore; + } + + public int getNumFailures() { + return numFailures; + } + + public void setNumFailures(int numFailures) { + this.updated |= this.numFailures != numFailures; + this.numFailures = numFailures; + } + + public long getLastFailure() { + return lastFailure; + } + + public void setLastFailure(long lastFailure) { + this.updated |= this.lastFailure != lastFailure; + this.lastFailure = lastFailure; + } + + public String getLastIPFailure() { + return lastIPFailure; + } + + public void setLastIPFailure(String lastIPFailure) { + this.updated |= !Objects.equals(this.lastIPFailure, lastIPFailure); + this.lastIPFailure = lastIPFailure; + } + + public void clearFailures() { + this.updated |= this.failedLoginNotBefore != 0 || this.numFailures != 0 || + this.lastFailure != 0l || this.lastIPFailure != null; + this.failedLoginNotBefore = this.numFailures = 0; + this.lastFailure = 0l; + this.lastIPFailure = null; + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/AbstractUserLoginFailureModel.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/AbstractUserLoginFailureModel.java new file mode 100644 index 0000000000..24a819fb13 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/AbstractUserLoginFailureModel.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.loginFailure; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserLoginFailureModel; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +/** + * @author Martin Kanis + */ +public abstract class AbstractUserLoginFailureModel implements UserLoginFailureModel { + protected final KeycloakSession session; + protected final RealmModel realm; + protected final E entity; + + public AbstractUserLoginFailureModel(KeycloakSession session, RealmModel realm, E entity) { + Objects.requireNonNull(entity, "entity"); + Objects.requireNonNull(realm, "realm"); + + this.session = session; + this.realm = realm; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserLoginFailureModel)) return false; + + MapUserLoginFailureAdapter that = (MapUserLoginFailureAdapter) o; + return Objects.equals(that.entity.getId(), entity.getId()); + } + + @Override + public int hashCode() { + return entity.getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureAdapter.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureAdapter.java new file mode 100644 index 0000000000..e87a71cfe4 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureAdapter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.loginFailure; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; + +/** + * @author Martin Kanis + */ +public class MapUserLoginFailureAdapter extends AbstractUserLoginFailureModel { + public MapUserLoginFailureAdapter(KeycloakSession session, RealmModel realm, MapUserLoginFailureEntity entity) { + super(session, realm, entity); + } + + @Override + public String getUserId() { + return entity.getUserId(); + } + + @Override + public int getFailedLoginNotBefore() { + return entity.getFailedLoginNotBefore(); + } + + @Override + public void setFailedLoginNotBefore(int notBefore) { + entity.setFailedLoginNotBefore(notBefore); + } + + @Override + public int getNumFailures() { + return entity.getNumFailures(); + } + + @Override + public void incrementFailures() { + entity.setNumFailures(getNumFailures() + 1); + } + + @Override + public void clearFailures() { + entity.clearFailures(); + } + + @Override + public long getLastFailure() { + return entity.getLastFailure(); + } + + @Override + public void setLastFailure(long lastFailure) { + entity.setLastFailure(lastFailure); + } + + @Override + public String getLastIPFailure() { + return entity.getLastIPFailure(); + } + + @Override + public void setLastIPFailure(String ip) { + entity.setLastIPFailure(ip); + } + + @Override + public String toString() { + return String.format("%s@%08x", entity.getId(), hashCode()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java new file mode 100644 index 0000000000..12656ed6c5 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.loginFailure; + +import java.util.UUID; + +/** + * @author Martin Kanis + */ +public class MapUserLoginFailureEntity extends AbstractUserLoginFailureEntity { + protected MapUserLoginFailureEntity() { + super(); + } + + public MapUserLoginFailureEntity(UUID id, String realmId, String userId) { + super(id, realmId, userId); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java new file mode 100644 index 0000000000..bcf8ed592a --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.loginFailure; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserLoginFailureProvider; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserLoginFailureModel; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; + +import java.util.UUID; +import java.util.function.Function; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; + +/** + * @author Martin Kanis + */ +public class MapUserLoginFailureProvider implements UserLoginFailureProvider { + + private static final Logger LOG = Logger.getLogger(MapUserLoginFailureProvider.class); + private final KeycloakSession session; + protected final MapKeycloakTransaction userLoginFailureTx; + private final MapStorage userLoginFailureStore; + + public MapUserLoginFailureProvider(KeycloakSession session, MapStorage userLoginFailureStore) { + this.session = session; + this.userLoginFailureStore = userLoginFailureStore; + + userLoginFailureTx = userLoginFailureStore.createTransaction(session); + session.getTransactionManager().enlistAfterCompletion(userLoginFailureTx); + } + + private Function userLoginFailureEntityToAdapterFunc(RealmModel realm) { + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return origEntity -> new MapUserLoginFailureAdapter(session, realm, registerEntityForChanges(origEntity)); + } + + private MapUserLoginFailureEntity registerEntityForChanges(MapUserLoginFailureEntity origEntity) { + MapUserLoginFailureEntity res = userLoginFailureTx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + userLoginFailureTx.updateIfChanged(origEntity.getId(), res, MapUserLoginFailureEntity::isUpdated); + return res; + } + + @Override + public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) { + ModelCriteriaBuilder mcb = userLoginFailureStore.createCriteriaBuilder() + .compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(UserLoginFailureModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, userId); + + LOG.tracef("getUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace()); + + return userLoginFailureTx.getUpdatedNotRemoved(mcb) + .findFirst() + .map(userLoginFailureEntityToAdapterFunc(realm)) + .orElse(null); + } + + @Override + public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) { + ModelCriteriaBuilder mcb = userLoginFailureStore.createCriteriaBuilder() + .compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(UserLoginFailureModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, userId); + + LOG.tracef("addUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace()); + + MapUserLoginFailureEntity userLoginFailureEntity = userLoginFailureTx.getUpdatedNotRemoved(mcb).findFirst().orElse(null); + + if (userLoginFailureEntity == null) { + userLoginFailureEntity = new MapUserLoginFailureEntity(UUID.randomUUID(), realm.getId(), userId); + + userLoginFailureTx.create(userLoginFailureEntity.getId(), userLoginFailureEntity); + } + + return userLoginFailureEntityToAdapterFunc(realm).apply(userLoginFailureEntity); + } + + @Override + public void removeUserLoginFailure(RealmModel realm, String userId) { + ModelCriteriaBuilder mcb = userLoginFailureStore.createCriteriaBuilder() + .compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(UserLoginFailureModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, userId); + + LOG.tracef("removeUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace()); + + userLoginFailureTx.delete(UUID.randomUUID(), mcb); + } + + @Override + public void removeAllUserLoginFailures(RealmModel realm) { + ModelCriteriaBuilder mcb = userLoginFailureStore.createCriteriaBuilder() + .compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()); + + LOG.tracef("removeAllUserLoginFailures(%s)%s", realm, getShortStackTrace()); + + userLoginFailureTx.delete(UUID.randomUUID(), mcb); + } + + @Override + public void close() { + + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java new file mode 100644 index 0000000000..6e216c3ee5 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.loginFailure; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserLoginFailureProvider; +import org.keycloak.models.UserLoginFailureProviderFactory; +import org.keycloak.models.UserLoginFailureModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.map.common.AbstractMapProviderFactory; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; + +import java.util.UUID; + +/** + * @author Martin Kanis + */ +public class MapUserLoginFailureProviderFactory extends AbstractMapProviderFactory + implements UserLoginFailureProviderFactory { + + private MapStorage userLoginFailureStore; + + @Override + public void postInit(KeycloakSessionFactory factory) { + MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class); + userLoginFailureStore = sp.getStorage("userLoginFailures", UUID.class, MapUserLoginFailureEntity.class, UserLoginFailureModel.class); + + factory.register(event -> { + if (event instanceof UserModel.UserRemovedEvent) { + UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; + + MapUserLoginFailureProvider provider = MapUserLoginFailureProviderFactory.this.create(userRemovedEvent.getKeycloakSession()); + provider.removeUserLoginFailure(userRemovedEvent.getRealm(), userRemovedEvent.getUser().getId()); + } + }); + + factory.register(event -> { + if (event instanceof RealmModel.RealmRemovedEvent) { + RealmModel.RealmRemovedEvent realmRemovedEvent = (RealmModel.RealmRemovedEvent) event; + + MapUserLoginFailureProvider provider = MapUserLoginFailureProviderFactory.this.create(realmRemovedEvent.getKeycloakSession()); + provider.removeAllUserLoginFailures(realmRemovedEvent.getRealm()); + } + }); + } + + @Override + public MapUserLoginFailureProvider create(KeycloakSession session) { + return new MapUserLoginFailureProvider(session, userLoginFailureStore); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java index ea84c79f59..00889efce5 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java @@ -1547,4 +1547,9 @@ public class MapRealmAdapter extends AbstractRealmModel implemen public OAuth2DeviceConfig getOAuth2DeviceConfig() { return new OAuth2DeviceConfig(this); } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java index 01d5c41886..bf049c1be0 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java @@ -21,12 +21,15 @@ import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.map.authSession.AbstractRootAuthenticationSessionEntity; import org.keycloak.models.map.authorization.entity.AbstractPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.AbstractPolicyEntity; @@ -39,6 +42,9 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.group.AbstractGroupEntity; import org.keycloak.models.map.realm.AbstractRealmEntity; import org.keycloak.models.map.role.AbstractRoleEntity; +import org.keycloak.models.map.userSession.AbstractAuthenticatedClientSessionEntity; +import org.keycloak.models.map.loginFailure.AbstractUserLoginFailureEntity; +import org.keycloak.models.map.userSession.AbstractUserSessionEntity; import org.keycloak.storage.SearchableModelField; import java.util.HashMap; import java.util.Map; @@ -55,6 +61,8 @@ import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; +import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID; + /** * * @author hmlnarik @@ -73,6 +81,9 @@ public class MapFieldPredicates { public static final Map, UpdatePredicatesFunc, Scope>> AUTHZ_SCOPE_PREDICATES = basePredicates(Scope.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, PermissionTicket>> AUTHZ_PERMISSION_TICKET_PREDICATES = basePredicates(PermissionTicket.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, Policy>> AUTHZ_POLICY_PREDICATES = basePredicates(Policy.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, UserSessionModel>> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, AuthenticatedClientSessionModel>> CLIENT_SESSION_PREDICATES = basePredicates(AuthenticatedClientSessionModel.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, UserLoginFailureModel>> USER_LOGIN_FAILURE_PREDICATES = basePredicates(UserLoginFailureModel.SearchableFields.ID); @SuppressWarnings("unchecked") private static final Map, Map> PREDICATES = new HashMap<>(); @@ -154,6 +165,24 @@ public class MapFieldPredicates { put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.SCOPE_ID, MapFieldPredicates::checkPolicyScopes); put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.CONFIG, MapFieldPredicates::checkPolicyConfig); put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.ASSOCIATED_POLICY_ID, MapFieldPredicates::checkAssociatedPolicy); + + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID, use -> use.getNote(CORRESPONDING_SESSION_ID)); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.REALM_ID, AbstractUserSessionEntity::getRealmId); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.USER_ID, AbstractUserSessionEntity::getUserId); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.CLIENT_ID, MapFieldPredicates::checkUserSessionContainsAuthenticatedClientSession); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.BROKER_SESSION_ID, AbstractUserSessionEntity::getBrokerSessionId); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.BROKER_USER_ID, AbstractUserSessionEntity::getBrokerUserId); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.IS_OFFLINE, AbstractUserSessionEntity::isOffline); + put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.LAST_SESSION_REFRESH, AbstractUserSessionEntity::getLastSessionRefresh); + + put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.REALM_ID, AbstractAuthenticatedClientSessionEntity::getRealmId); + put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.CLIENT_ID, AbstractAuthenticatedClientSessionEntity::getClientId); + put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, AbstractAuthenticatedClientSessionEntity::getUserSessionId); + put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.IS_OFFLINE, AbstractAuthenticatedClientSessionEntity::isOffline); + put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.TIMESTAMP, AbstractAuthenticatedClientSessionEntity::getTimestamp); + + put(USER_LOGIN_FAILURE_PREDICATES, UserLoginFailureModel.SearchableFields.REALM_ID, AbstractUserLoginFailureEntity::getRealmId); + put(USER_LOGIN_FAILURE_PREDICATES, UserLoginFailureModel.SearchableFields.USER_ID, AbstractUserLoginFailureEntity::getUserId); } static { @@ -169,6 +198,9 @@ public class MapFieldPredicates { PREDICATES.put(Scope.class, AUTHZ_SCOPE_PREDICATES); PREDICATES.put(PermissionTicket.class, AUTHZ_PERMISSION_TICKET_PREDICATES); PREDICATES.put(Policy.class, AUTHZ_POLICY_PREDICATES); + PREDICATES.put(UserSessionModel.class, USER_SESSION_PREDICATES); + PREDICATES.put(AuthenticatedClientSessionModel.class, CLIENT_SESSION_PREDICATES); + PREDICATES.put(UserLoginFailureModel.class, USER_LOGIN_FAILURE_PREDICATES); } private static , M> void put( @@ -423,6 +455,13 @@ public class MapFieldPredicates { private static MapModelCriteriaBuilder, RealmModel> checkRealmsWithComponentType(MapModelCriteriaBuilder, RealmModel> mcb, Operator op, Object[] values) { String providerType = ensureEqSingleValue(RealmModel.SearchableFields.COMPONENT_PROVIDER_TYPE, "component_provider_type", op, values); Function, ?> getter = realmEntity -> realmEntity.getComponents().anyMatch(component -> component.getProviderType().equals(providerType)); + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + + private static MapModelCriteriaBuilder, UserSessionModel> checkUserSessionContainsAuthenticatedClientSession(MapModelCriteriaBuilder, UserSessionModel> mcb, Operator op, Object[] values) { + String clientId = ensureEqSingleValue(UserSessionModel.SearchableFields.CLIENT_ID, "client_id", op, values); + Function, ?> getter; + getter = use -> (use.getAuthenticatedClientSessions().containsKey(clientId)); return mcb.fieldCompare(Boolean.TRUE::equals, getter); } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java index a5b406c605..52f22a6a4f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java @@ -388,7 +388,7 @@ public class MapKeycloakTransaction, M> implement Predicate entityFilter = mmcb.getEntityFilter(); Predicate keyFilter = ((MapModelCriteriaBuilder) mcb).getKeyFilter(); - return v -> v != null && ! (keyFilter.test(v.getId()) && entityFilter.test(v)); + return v -> v == null || ! (keyFilter.test(v.getId()) && entityFilter.test(v)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java index 27d3b26aa0..1e607a2231 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java @@ -17,8 +17,10 @@ package org.keycloak.models.map.storage.chm; import org.keycloak.Config.Scope; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.Serialization; import com.fasterxml.jackson.databind.JavaType; @@ -31,6 +33,8 @@ import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity; +import java.util.UUID; /** * @@ -92,7 +96,14 @@ public class ConcurrentHashMapStorageProvider implements MapStorageProvider { private , M> ConcurrentHashMapStorage loadMap(String fileName, Class valueType, Class modelType, EnumSet flags) { - ConcurrentHashMapStorage store = new ConcurrentHashMapStorage<>(modelType); + ConcurrentHashMapStorage store; + if (modelType == UserSessionModel.class) { + ConcurrentHashMapStorage clientSessionStore = + getStorage("clientSessions", UUID.class, MapAuthenticatedClientSessionEntity.class, AuthenticatedClientSessionModel.class); + store = new UserSessionConcurrentHashMapStorage<>(clientSessionStore); + } else { + store = new ConcurrentHashMapStorage<>(modelType); + } if (! flags.contains(Flag.INITIALIZE_EMPTY)) { final File f = getFile(fileName); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java new file mode 100644 index 0000000000..9aa637adfc --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.storage.chm; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; +import org.keycloak.models.map.userSession.AbstractAuthenticatedClientSessionEntity; +import org.keycloak.models.map.userSession.AbstractUserSessionEntity; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * User session storage with a naive implementation of referential integrity in client to user session relation, restricted to + * ON DELETE CASCADE functionality. + * + * @author hmlnarik + */ +public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapStorage, UserSessionModel> { + + private final ConcurrentHashMapStorage, AuthenticatedClientSessionModel> clientSessionStore; + + private class Transaction extends MapKeycloakTransaction, UserSessionModel> { + + private final MapKeycloakTransaction, AuthenticatedClientSessionModel> clientSessionTr; + + public Transaction(MapKeycloakTransaction, AuthenticatedClientSessionModel> clientSessionTr) { + super(UserSessionConcurrentHashMapStorage.this); + this.clientSessionTr = clientSessionTr; + } + + @Override + public long delete(K artificialKey, ModelCriteriaBuilder mcb) { + Set ids = getUpdatedNotRemoved(mcb).map(AbstractEntity::getId).collect(Collectors.toSet()); + ModelCriteriaBuilder csMcb = clientSessionStore.createCriteriaBuilder().compare(AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, Operator.IN, ids); + clientSessionTr.delete(artificialKey, csMcb); + return super.delete(artificialKey, mcb); + } + + @Override + public void delete(K key) { + ModelCriteriaBuilder csMcb = clientSessionStore.createCriteriaBuilder().compare(AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, Operator.EQ, key); + clientSessionTr.delete(key, csMcb); + super.delete(key); + } + + } + + @SuppressWarnings("unchecked") + public UserSessionConcurrentHashMapStorage(ConcurrentHashMapStorage, AuthenticatedClientSessionModel> clientSessionStore) { + super(UserSessionModel.class); + this.clientSessionStore = clientSessionStore; + } + + @Override + @SuppressWarnings("unchecked") + public MapKeycloakTransaction, UserSessionModel> createTransaction(KeycloakSession session) { + MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); + return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session)) : (MapKeycloakTransaction, UserSessionModel>) sessionTransaction; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java index 48dab2a92c..cd48bca631 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java @@ -304,4 +304,9 @@ public abstract class MapUserAdapter extends AbstractUserModel { public void deleteRoleMapping(RoleModel role) { entity.removeRolesMembership(role.getId()); } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } } diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionEntity.java new file mode 100644 index 0000000000..f186a70ece --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionEntity.java @@ -0,0 +1,205 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.common.util.Time; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Martin Kanis + */ +public abstract class AbstractAuthenticatedClientSessionEntity implements AbstractEntity { + + private K id; + private String userSessionId; + private String realmId; + private String clientId; + + /** + * Flag signalizing that any of the setters has been meaningfully used. + */ + protected boolean updated; + + private String authMethod; + private String redirectUri; + private volatile int timestamp; + private long expiration; + private String action; + + private Map notes = new ConcurrentHashMap<>(); + + private String currentRefreshToken; + private int currentRefreshTokenUseCount; + + private boolean offline; + + public AbstractAuthenticatedClientSessionEntity() { + this.id = null; + this.realmId = null; + } + + public AbstractAuthenticatedClientSessionEntity(K id, String userSessionId, String realmId, String clientId, boolean offline) { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(userSessionId, "userSessionId"); + Objects.requireNonNull(realmId, "realmId"); + Objects.requireNonNull(clientId, "clientId"); + + this.id = id; + this.userSessionId = userSessionId; + this.realmId = realmId; + this.clientId = clientId; + this.offline = offline; + this.timestamp = Time.currentTime(); + } + + @Override + public K getId() { + return this.id; + } + + @Override + public boolean isUpdated() { + return this.updated; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.updated |= !Objects.equals(this.realmId, realmId); + this.realmId = realmId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.updated |= !Objects.equals(this.clientId, clientId); + this.clientId = clientId; + } + + public String getUserSessionId() { + return userSessionId; + } + + public void setUserSessionId(String userSessionId) { + this.updated |= !Objects.equals(this.userSessionId, userSessionId); + this.userSessionId = userSessionId; + } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.updated |= !Objects.equals(this.authMethod, authMethod); + this.authMethod = authMethod; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.updated |= !Objects.equals(this.redirectUri, redirectUri); + this.redirectUri = redirectUri; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.updated |= this.timestamp != timestamp; + this.timestamp = timestamp; + } + + public long getExpiration() { + return expiration; + } + + public void setExpiration(long expiration) { + this.updated |= this.expiration != expiration; + this.expiration = expiration; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.updated |= !Objects.equals(this.action, action); + this.action = action; + } + + public Map getNotes() { + return notes; + } + + public void setNotes(Map notes) { + this.updated |= !Objects.equals(this.notes, notes); + this.notes = notes; + } + + public String removeNote(String name) { + String note = this.notes.remove(name); + this.updated |= note != null; + return note; + } + + public void addNote(String name, String value) { + this.updated |= !Objects.equals(this.notes.put(name, value), value); + } + + public String getCurrentRefreshToken() { + return currentRefreshToken; + } + + public void setCurrentRefreshToken(String currentRefreshToken) { + this.updated |= !Objects.equals(this.currentRefreshToken, currentRefreshToken); + this.currentRefreshToken = currentRefreshToken; + } + + public int getCurrentRefreshTokenUseCount() { + return currentRefreshTokenUseCount; + } + + public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { + this.updated |= this.currentRefreshTokenUseCount != currentRefreshTokenUseCount; + this.currentRefreshTokenUseCount = currentRefreshTokenUseCount; + } + + public boolean isOffline() { + return offline; + } + + public void setOffline(boolean offline) { + this.updated |= this.offline != offline; + this.offline = offline; + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionModel.java b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionModel.java new file mode 100644 index 0000000000..47942cb1c2 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractAuthenticatedClientSessionModel.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +/** + * @author Martin Kanis + */ +public abstract class AbstractAuthenticatedClientSessionModel implements AuthenticatedClientSessionModel { + protected final KeycloakSession session; + protected final RealmModel realm; + protected ClientModel client; + protected UserSessionModel userSession; + protected final E entity; + + public AbstractAuthenticatedClientSessionModel(KeycloakSession session, RealmModel realm, ClientModel client, + UserSessionModel userSession, E entity) { + Objects.requireNonNull(entity, "entity"); + Objects.requireNonNull(realm, "realm"); + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(userSession, "userSession"); + + this.session = session; + this.realm = realm; + this.client = client; + this.userSession = userSession; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AuthenticatedClientSessionModel)) return false; + + AuthenticatedClientSessionModel that = (AuthenticatedClientSessionModel) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionEntity.java new file mode 100644 index 0000000000..16b5db18e6 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionEntity.java @@ -0,0 +1,288 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.common.util.Time; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Martin Kanis + */ +public abstract class AbstractUserSessionEntity implements AbstractEntity { + private K id; + + private String realmId; + + /** + * Flag signalizing that any of the setters has been meaningfully used. + */ + protected boolean updated; + + private String userId; + + private String brokerSessionId; + private String brokerUserId; + + private String loginUsername; + + private String ipAddress; + + private String authMethod; + + private boolean rememberMe; + + private int started; + + private int lastSessionRefresh; + + private long expiration; + + private Map notes = new ConcurrentHashMap<>(); + + private UserSessionModel.State state; + + private UserSessionModel.SessionPersistenceState persistenceState = UserSessionModel.SessionPersistenceState.PERSISTENT; + + private Map authenticatedClientSessions = new ConcurrentHashMap<>(); + + private boolean offline; + + public AbstractUserSessionEntity() { + this.id = null; + this.realmId = null; + } + + public AbstractUserSessionEntity(K id, String realmId) { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(realmId, "realmId"); + + this.id = id; + this.realmId = realmId; + } + + public AbstractUserSessionEntity(K id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, + String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, + boolean offline) { + this.id = id; + this.realmId = realm.getId(); + this.userId = user.getId(); + this.loginUsername = loginUsername; + this.ipAddress = ipAddress; + this.authMethod = authMethod; + this.rememberMe = rememberMe; + this.brokerSessionId = brokerSessionId; + this.brokerUserId = brokerUserId; + this.started = Time.currentTime(); + this.lastSessionRefresh = started; + this.offline = offline; + } + + @Override + public K getId() { + return this.id; + } + + @Override + public boolean isUpdated() { + return this.updated; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.updated |= !Objects.equals(this.realmId, realmId); + this.realmId = realmId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.updated |= !Objects.equals(this.userId, userId); + this.userId = userId; + } + + public String getBrokerSessionId() { + return brokerSessionId; + } + + public void setBrokerSessionId(String brokerSessionId) { + this.updated |= !Objects.equals(this.brokerSessionId, brokerSessionId); + this.brokerSessionId = brokerSessionId; + } + + public String getBrokerUserId() { + return brokerUserId; + } + + public void setBrokerUserId(String brokerUserId) { + this.updated |= !Objects.equals(this.brokerUserId, brokerUserId); + this.brokerUserId = brokerUserId; + } + + public String getLoginUsername() { + return loginUsername; + } + + public void setLoginUsername(String loginUsername) { + this.updated |= !Objects.equals(this.loginUsername, loginUsername); + this.loginUsername = loginUsername; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.updated |= !Objects.equals(this.ipAddress, ipAddress); + this.ipAddress = ipAddress; + } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.updated |= !Objects.equals(this.authMethod, authMethod); + this.authMethod = authMethod; + } + + public boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(boolean rememberMe) { + this.updated |= this.rememberMe != rememberMe; + this.rememberMe = rememberMe; + } + + public int getStarted() { + return started; + } + + public void setStarted(int started) { + this.updated |= this.started != started; + this.started = started; + } + + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + public void setLastSessionRefresh(int lastSessionRefresh) { + this.updated |= this.lastSessionRefresh != lastSessionRefresh; + this.lastSessionRefresh = lastSessionRefresh; + } + + public long getExpiration() { + return expiration; + } + + public void setExpiration(long expiration) { + this.updated |= this.expiration != expiration; + this.expiration = expiration; + } + + public Map getNotes() { + return notes; + } + + public String getNote(String name) { + return notes.get(name); + } + + public void setNotes(Map notes) { + this.updated |= !Objects.equals(this.notes, notes); + this.notes = notes; + } + + public String removeNote(String name) { + String note = this.notes.remove(name); + this.updated |= note != null; + return note; + } + + public void addNote(String name, String value) { + this.updated |= !Objects.equals(this.notes.put(name, value), value); + } + + public UserSessionModel.State getState() { + return state; + } + + public void setState(UserSessionModel.State state) { + this.updated |= !Objects.equals(this.state, state); + this.state = state; + } + + public Map getAuthenticatedClientSessions() { + return authenticatedClientSessions; + } + + public void setAuthenticatedClientSessions(Map authenticatedClientSessions) { + this.updated |= !Objects.equals(this.authenticatedClientSessions, authenticatedClientSessions); + this.authenticatedClientSessions = authenticatedClientSessions; + } + + public void addAuthenticatedClientSession(String clientId, K clientSessionId) { + this.updated |= !Objects.equals(this.authenticatedClientSessions.put(clientId, clientSessionId), clientSessionId); + } + + public K removeAuthenticatedClientSession(String clientId) { + K entity = this.authenticatedClientSessions.remove(clientId); + this.updated |= entity != null; + return entity; + } + + public void clearAuthenticatedClientSessions() { + this.updated |= !authenticatedClientSessions.isEmpty(); + this.authenticatedClientSessions.clear(); + } + + public boolean isOffline() { + return offline; + } + + public void setOffline(boolean offline) { + this.updated |= this.offline != offline; + this.offline = offline; + } + + public UserSessionModel.SessionPersistenceState getPersistenceState() { + return persistenceState; + } + + public void setPersistenceState(UserSessionModel.SessionPersistenceState persistenceState) { + this.updated |= !Objects.equals(this.persistenceState, persistenceState); + this.persistenceState = persistenceState; + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionModel.java b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionModel.java new file mode 100644 index 0000000000..44b5b76ccb --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/AbstractUserSessionModel.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +/** + * @author Martin Kanis + */ +public abstract class AbstractUserSessionModel implements UserSessionModel { + protected final KeycloakSession session; + protected final RealmModel realm; + protected final E entity; + + public AbstractUserSessionModel(KeycloakSession session, RealmModel realm, E entity) { + Objects.requireNonNull(entity, "entity"); + Objects.requireNonNull(realm, "realm"); + + this.session = session; + this.realm = realm; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserSessionModel)) return false; + + UserSessionModel that = (UserSessionModel) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionAdapter.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionAdapter.java new file mode 100644 index 0000000000..c9bded203b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionAdapter.java @@ -0,0 +1,148 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; + +import java.util.Map; + +/** + * @author Martin Kanis + */ +public abstract class MapAuthenticatedClientSessionAdapter extends AbstractAuthenticatedClientSessionModel { + + public MapAuthenticatedClientSessionAdapter(KeycloakSession session, RealmModel realm, ClientModel client, + UserSessionModel userSession, MapAuthenticatedClientSessionEntity entity) { + super(session, realm, client, userSession, entity); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + } + + @Override + public UserSessionModel getUserSession() { + return userSession; + } + + @Override + public String getCurrentRefreshToken() { + return entity.getCurrentRefreshToken(); + } + + @Override + public void setCurrentRefreshToken(String currentRefreshToken) { + entity.setCurrentRefreshToken(currentRefreshToken); + } + + @Override + public int getCurrentRefreshTokenUseCount() { + return entity.getCurrentRefreshTokenUseCount(); + } + + @Override + public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { + entity.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount); + } + + @Override + public String getNote(String name) { + return (name != null) ? entity.getNotes().get(name) : null; + } + + @Override + public void setNote(String name, String value) { + if (name != null) { + if (value == null) { + entity.removeNote(name); + } else { + entity.addNote(name, value); + } + } + } + + @Override + public void removeNote(String name) { + if (name != null) { + entity.removeNote(name); + } + } + + @Override + public Map getNotes() { + return entity.getNotes(); + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public void setRedirectUri(String uri) { + entity.setRedirectUri(uri); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public ClientModel getClient() { + return client; + } + + @Override + public String getAction() { + return entity.getAction(); + } + + @Override + public void setAction(String action) { + entity.setAction(action); + } + + @Override + public String getProtocol() { + return entity.getAuthMethod(); + } + + @Override + public void setProtocol(String method) { + entity.setAuthMethod(method); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java new file mode 100644 index 0000000000..e9eed706fb --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import java.util.UUID; + +/** + * @author Martin Kanis + */ +public class MapAuthenticatedClientSessionEntity extends AbstractAuthenticatedClientSessionEntity { + + protected MapAuthenticatedClientSessionEntity() { + super(); + } + + public MapAuthenticatedClientSessionEntity(UUID id, String userSessionId, String realmId, String clientId, boolean offline) { + super(id, userSessionId, realmId, clientId, offline); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionAdapter.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionAdapter.java new file mode 100644 index 0000000000..580fe28e84 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionAdapter.java @@ -0,0 +1,223 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Martin Kanis + */ +public abstract class MapUserSessionAdapter extends AbstractUserSessionModel { + + public MapUserSessionAdapter(KeycloakSession session, RealmModel realm, MapUserSessionEntity entity) { + super(session, realm, entity); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public String getBrokerSessionId() { + return entity.getBrokerSessionId(); + } + + @Override + public String getBrokerUserId() { + return entity.getBrokerUserId(); + } + + @Override + public UserModel getUser() { + return session.users().getUserById(getRealm(), entity.getUserId()); + } + + @Override + public String getLoginUsername() { + return entity.getLoginUsername(); + } + + @Override + public String getIpAddress() { + return entity.getIpAddress(); + } + + @Override + public String getAuthMethod() { + return entity.getAuthMethod(); + } + + @Override + public boolean isRememberMe() { + return entity.isRememberMe(); + } + + @Override + public int getStarted() { + return entity.getStarted(); + } + + @Override + public int getLastSessionRefresh() { + return entity.getLastSessionRefresh(); + } + + @Override + public void setLastSessionRefresh(int seconds) { + entity.setLastSessionRefresh(seconds); + } + + @Override + public boolean isOffline() { + return entity.isOffline(); + } + + @Override + public Map getAuthenticatedClientSessions() { + Map result = new HashMap<>(); + List removedClientUUIDS = new LinkedList<>(); + + entity.getAuthenticatedClientSessions().entrySet() + .stream() + .forEach(entry -> { + String clientUUID = entry.getKey(); + ClientModel client = realm.getClientById(clientUUID); + + if (client != null) { + AuthenticatedClientSessionModel clientSession = session.sessions() + .getClientSession(this, client, entry.getValue(), isOffline()); + if (clientSession != null) { + result.put(clientUUID, clientSession); + } + } else { + removedClientUUIDS.add(clientUUID); + } + }); + + removeAuthenticatedClientSessions(removedClientUUIDS); + + return Collections.unmodifiableMap(result); + } + + @Override + public AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) { + UUID clientSessionId = entity.getAuthenticatedClientSessions().get(clientUUID); + + if (clientSessionId == null) { + return null; + } + + ClientModel client = realm.getClientById(clientUUID); + + if (client != null) { + return session.sessions().getClientSession(this, client, clientSessionId, isOffline()); + } + + removeAuthenticatedClientSessions(Collections.singleton(clientUUID)); + + return null; + } + + + @Override + public String getNote(String name) { + return (name != null) ? entity.getNotes().get(name) : null; + } + + @Override + public void setNote(String name, String value) { + if (name != null) { + if (value == null) { + entity.removeNote(name); + } else { + entity.addNote(name, value); + } + } + } + + @Override + public void removeNote(String name) { + if (name != null) { + entity.removeNote(name); + } + } + + @Override + public Map getNotes() { + return entity.getNotes(); + } + + @Override + public State getState() { + return entity.getState(); + } + + @Override + public void setState(State state) { + entity.setState(state); + } + + @Override + public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, + boolean rememberMe, String brokerSessionId, String brokerUserId) { + entity.setRealmId(realm.getId()); + entity.setUserId(user.getId()); + entity.setLoginUsername(loginUsername); + entity.setIpAddress(ipAddress); + entity.setAuthMethod(authMethod); + entity.setRememberMe(rememberMe); + entity.setBrokerSessionId(brokerSessionId); + entity.setBrokerUserId(brokerUserId); + + int currentTime = Time.currentTime(); + entity.setStarted(currentTime); + entity.setLastSessionRefresh(currentTime); + + entity.setState(null); + + String correspondingSessionId = entity.getNote(CORRESPONDING_SESSION_ID); + entity.setNotes(new ConcurrentHashMap<>()); + if (correspondingSessionId != null) + entity.addNote(CORRESPONDING_SESSION_ID, correspondingSessionId); + + entity.clearAuthenticatedClientSessions(); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), hashCode()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java new file mode 100644 index 0000000000..2142d1615d --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import java.util.UUID; + +/** + * @author Martin Kanis + */ +public class MapUserSessionEntity extends AbstractUserSessionEntity { + protected MapUserSessionEntity() { + super(); + } + + public MapUserSessionEntity(UUID id, String realmId) { + super(id, realmId); + } + + public MapUserSessionEntity(UUID id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, + String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, + boolean offline) { + super(id, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId, offline); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProvider.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProvider.java new file mode 100644 index 0000000000..cd54773cda --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProvider.java @@ -0,0 +1,680 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.device.DeviceActivityManager; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID; +import static org.keycloak.models.UserSessionModel.SessionPersistenceState.TRANSIENT; +import static org.keycloak.models.map.userSession.SessionExpiration.setClientSessionExpiration; +import static org.keycloak.models.map.userSession.SessionExpiration.setUserSessionExpiration; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +/** + * @author Martin Kanis + */ +public class MapUserSessionProvider implements UserSessionProvider { + + private static final Logger LOG = Logger.getLogger(MapUserSessionProvider.class); + private final KeycloakSession session; + protected final MapKeycloakTransaction userSessionTx; + protected final MapKeycloakTransaction clientSessionTx; + private final MapStorage userSessionStore; + private final MapStorage clientSessionStore; + + /** + * Storage for transient user sessions which lifespan is limited to one request. + */ + private final Map transientUserSessions = new HashMap<>(); + + public MapUserSessionProvider(KeycloakSession session, MapStorage userSessionStore, + MapStorage clientSessionStore) { + this.session = session; + this.userSessionStore = userSessionStore; + this.clientSessionStore = clientSessionStore; + userSessionTx = userSessionStore.createTransaction(session); + clientSessionTx = clientSessionStore.createTransaction(session); + + session.getTransactionManager().enlistAfterCompletion(userSessionTx); + session.getTransactionManager().enlistAfterCompletion(clientSessionTx); + } + + private Function userEntityToAdapterFunc(RealmModel realm) { + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return (origEntity) -> { + if (origEntity.getExpiration() <= Time.currentTime()) { + if (Objects.equals(origEntity.getPersistenceState(), TRANSIENT)) { + transientUserSessions.remove(origEntity.getId()); + } + userSessionTx.delete(origEntity.getId()); + return null; + } else { + return new MapUserSessionAdapter(session, realm, + Objects.equals(origEntity.getPersistenceState(), TRANSIENT) ? origEntity : registerEntityForChanges(origEntity)) { + + @Override + public void removeAuthenticatedClientSessions(Collection removedClientUUIDS) { + removedClientUUIDS.forEach(entity::removeAuthenticatedClientSession); + } + + @Override + public void setLastSessionRefresh(int lastSessionRefresh) { + entity.setLastSessionRefresh(lastSessionRefresh); + // whenever the lastSessionRefresh is changed recompute the expiration time + setUserSessionExpiration(entity, realm); + } + }; + } + }; + } + + private Function clientEntityToAdapterFunc(RealmModel realm, + ClientModel client, + UserSessionModel userSession) { + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return origEntity -> { + if (origEntity.getExpiration() <= Time.currentTime()) { + userSession.removeAuthenticatedClientSessions(Arrays.asList(origEntity.getClientId())); + clientSessionTx.delete(origEntity.getId()); + return null; + } else { + return new MapAuthenticatedClientSessionAdapter(session, realm, client, userSession, registerEntityForChanges(origEntity)) { + @Override + public void detachFromUserSession() { + this.userSession = null; + + clientSessionTx.delete(entity.getId()); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + // whenever the timestamp is changed recompute the expiration time + setClientSessionExpiration(entity, realm, client); + } + }; + } + }; + } + + private MapUserSessionEntity registerEntityForChanges(MapUserSessionEntity origEntity) { + MapUserSessionEntity res = userSessionTx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + userSessionTx.updateIfChanged(origEntity.getId(), res, MapUserSessionEntity::isUpdated); + return res; + } + + private MapAuthenticatedClientSessionEntity registerEntityForChanges(MapAuthenticatedClientSessionEntity origEntity) { + MapAuthenticatedClientSessionEntity res = clientSessionTx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + clientSessionTx.updateIfChanged(origEntity.getId(), res, MapAuthenticatedClientSessionEntity::isUpdated); + return res; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + + @Override + public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { + MapAuthenticatedClientSessionEntity entity = + new MapAuthenticatedClientSessionEntity(UUID.randomUUID(), userSession.getId(), realm.getId(), client.getId(), false); + setClientSessionExpiration(entity, realm, client); + + LOG.tracef("createClientSession(%s, %s, %s)%s", realm, client, userSession, getShortStackTrace()); + + clientSessionTx.create(entity.getId(), entity); + + MapUserSessionEntity userSessionEntity = getUserSessionById(UUID.fromString(userSession.getId())); + + if (userSessionEntity == null) { + throw new IllegalStateException("User session entity does not exist: " + userSession.getId()); + } + + userSessionEntity.addAuthenticatedClientSession(client.getId(), entity.getId()); + + return clientEntityToAdapterFunc(realm, client, userSession).apply(entity); + } + + @Override + public AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, + UUID clientSessionId, boolean offline) { + LOG.tracef("getClientSession(%s, %s, %s, %s)%s", userSession, client, + clientSessionId, offline, getShortStackTrace()); + + Objects.requireNonNull(userSession, "The provided user session cannot be null!"); + Objects.requireNonNull(client, "The provided client cannot be null!"); + if (clientSessionId == null) { + return null; + } + + ModelCriteriaBuilder mcb = clientSessionStore.createCriteriaBuilder() + .compare(AuthenticatedClientSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, clientSessionId) + .compare(AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, userSession.getId()) + .compare(AuthenticatedClientSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, userSession.getRealm().getId()) + .compare(AuthenticatedClientSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId()) + .compare(AuthenticatedClientSessionModel.SearchableFields.IS_OFFLINE, ModelCriteriaBuilder.Operator.EQ, offline); + + return clientSessionTx.getUpdatedNotRemoved(mcb) + .findFirst() + .map(clientEntityToAdapterFunc(client.getRealm(), client, userSession)) + .orElse(null); + } + + @Override + public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, + String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + return createUserSession(null, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, + brokerUserId, UserSessionModel.SessionPersistenceState.PERSISTENT); + } + + @Override + public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, + String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, + String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) { + final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id); + + LOG.tracef("createUserSession(%s, %s, %s, %s)%s", id, realm, loginUsername, persistenceState, getShortStackTrace()); + + MapUserSessionEntity entity = new MapUserSessionEntity(entityId, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId, false); + entity.setPersistenceState(persistenceState); + setUserSessionExpiration(entity, realm); + + if (Objects.equals(persistenceState, TRANSIENT)) { + transientUserSessions.put(entityId, entity); + } else { + if (userSessionTx.read(entity.getId()) != null) { + throw new ModelDuplicateException("User session exists: " + entity.getId()); + } + + userSessionTx.create(entity.getId(), entity); + } + + UserSessionModel userSession = userEntityToAdapterFunc(realm).apply(entity); + + if (userSession != null) { + DeviceActivityManager.attachDevice(userSession, session); + } + + return userSession; + } + + @Override + public UserSessionModel getUserSession(RealmModel realm, String id) { + Objects.requireNonNull(realm, "The provided realm can't be null!"); + + LOG.tracef("getUserSession(%s, %s)%s", realm, id, getShortStackTrace()); + + UUID uuid = toUUID(id); + if (uuid == null) { + return null; + } + + MapUserSessionEntity userSessionEntity = transientUserSessions.get(uuid); + if (userSessionEntity != null) { + return userEntityToAdapterFunc(realm).apply(userSessionEntity); + } + + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, uuid); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .findFirst() + .map(userEntityToAdapterFunc(realm)) + .orElse(null); + } + + @Override + public Stream getUserSessionsStream(RealmModel realm, UserModel user) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, user.getId()); + + LOG.tracef("getUserSessionsStream(%s, %s)%s", realm, user, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull); + } + + @Override + public Stream getUserSessionsStream(RealmModel realm, ClientModel client) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId()); + + LOG.tracef("getUserSessionsStream(%s, %s)%s", realm, client, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull); + } + + @Override + public Stream getUserSessionsStream(RealmModel realm, ClientModel client, + Integer firstResult, Integer maxResults) { + return paginatedStream(getUserSessionsStream(realm, client) + .sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh)), firstResult, maxResults); + } + + @Override + public Stream getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.BROKER_USER_ID, ModelCriteriaBuilder.Operator.EQ, brokerUserId); + + LOG.tracef("getUserSessionByBrokerUserIdStream(%s, %s)%s", realm, brokerUserId, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull); + } + + @Override + public UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.BROKER_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, brokerSessionId); + + LOG.tracef("getUserSessionByBrokerSessionId(%s, %s)%s", realm, brokerSessionId, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .findFirst() + .map(userEntityToAdapterFunc(realm)) + .orElse(null); + } + + @Override + public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, + Predicate predicate) { + LOG.tracef("getUserSessionWithPredicate(%s, %s, %s)%s", realm, id, offline, getShortStackTrace()); + + Stream userSessionEntityStream; + if (offline) { + userSessionEntityStream = getOfflineUserSessionEntityStream(realm, id) + .map(userEntityToAdapterFunc(realm)).filter(Objects::nonNull); + } else { + UserSessionModel userSession = getUserSession(realm, id); + userSessionEntityStream = userSession != null ? Stream.of(userSession) : Stream.empty(); + } + + return userSessionEntityStream + .filter(predicate) + .findFirst() + .orElse(null); + } + + @Override + public long getActiveUserSessions(RealmModel realm, ClientModel client) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId()); + + LOG.tracef("getActiveUserSessions(%s, %s)%s", realm, client, getShortStackTrace()); + + return userSessionTx.getCount(mcb); + } + + @Override + public Map getActiveClientSessionStats(RealmModel realm, boolean offline) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, offline); + + LOG.tracef("getActiveClientSessionStats(%s, %s)%s", realm, offline, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull) + .map(UserSessionModel::getAuthenticatedClientSessions) + .map(Map::keySet) + .flatMap(Collection::stream) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + } + + @Override + public void removeUserSession(RealmModel realm, UserSessionModel session) { + Objects.requireNonNull(session, "The provided user session can't be null!"); + + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false) + .compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, UUID.fromString(session.getId())); + + LOG.tracef("removeUserSession(%s, %s)%s", realm, session, getShortStackTrace()); + + userSessionTx.delete(UUID.randomUUID(), mcb); + } + + @Override + public void removeUserSessions(RealmModel realm, UserModel user) { + ModelCriteriaBuilder mcb = userSessionStore.createCriteriaBuilder() + .compare(UserSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(UserSessionModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, user.getId()); + + LOG.tracef("removeUserSessions(%s, %s)%s", realm, user, getShortStackTrace()); + + userSessionTx.delete(UUID.randomUUID(), mcb); + } + + @Override + public void removeAllExpired() { + LOG.tracef("removeAllExpired()%s", getShortStackTrace()); + } + + @Override + public void removeExpired(RealmModel realm) { + LOG.tracef("removeExpired(%s)%s", realm, getShortStackTrace()); + } + + @Override + public void removeUserSessions(RealmModel realm) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, false); + + LOG.tracef("removeUserSessions(%s)%s", realm, getShortStackTrace()); + + userSessionTx.delete(UUID.randomUUID(), mcb); + } + + @Override + public void onRealmRemoved(RealmModel realm) { + LOG.tracef("onRealmRemoved(%s)%s", realm, getShortStackTrace()); + + removeUserSessions(realm); + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + + } + + @Override + public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { + LOG.tracef("createOfflineUserSession(%s)%s", userSession, getShortStackTrace()); + + MapUserSessionEntity offlineUserSession = createUserSessionEntityInstance(userSession, true); + + // set a reference for the offline user session to the original online user session + userSession.setNote(CORRESPONDING_SESSION_ID, offlineUserSession.getId().toString()); + + int currentTime = Time.currentTime(); + offlineUserSession.setStarted(currentTime); + offlineUserSession.setLastSessionRefresh(currentTime); + setUserSessionExpiration(offlineUserSession, userSession.getRealm()); + + userSessionTx.create(offlineUserSession.getId(), offlineUserSession); + + return userEntityToAdapterFunc(userSession.getRealm()).apply(offlineUserSession); + } + + @Override + public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) { + LOG.tracef("getOfflineUserSession(%s, %s)%s", realm, userSessionId, getShortStackTrace()); + + return getOfflineUserSessionEntityStream(realm, userSessionId) + .findFirst() + .map(userEntityToAdapterFunc(realm)) + .orElse(null); + } + + @Override + public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) { + Objects.requireNonNull(userSession, "The provided user session can't be null!"); + + LOG.tracef("removeOfflineUserSession(%s, %s)%s", realm, userSession, getShortStackTrace()); + + ModelCriteriaBuilder mcb; + if (userSession.isOffline()) { + userSessionTx.delete(UUID.fromString(userSession.getId())); + } else if (userSession.getNote(CORRESPONDING_SESSION_ID) != null) { + mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, UUID.fromString(userSession.getNote(CORRESPONDING_SESSION_ID))); + userSessionTx.delete(UUID.randomUUID(), mcb); + userSession.removeNote(CORRESPONDING_SESSION_ID); + } + } + + @Override + public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, + UserSessionModel offlineUserSession) { + LOG.tracef("createOfflineClientSession(%s, %s)%s", clientSession, offlineUserSession, getShortStackTrace()); + + MapAuthenticatedClientSessionEntity clientSessionEntity = createAuthenticatedClientSessionInstance(clientSession, offlineUserSession, true); + clientSessionEntity.setTimestamp(Time.currentTime()); + setClientSessionExpiration(clientSessionEntity, clientSession.getRealm(), clientSession.getClient()); + + Optional userSessionEntity = getOfflineUserSessionEntityStream(clientSession.getRealm(), offlineUserSession.getId()).findFirst(); + if (userSessionEntity.isPresent()) { + userSessionEntity.get().addAuthenticatedClientSession(clientSession.getClient().getId(), clientSessionEntity.getId()); + } + + clientSessionTx.create(clientSessionEntity.getId(), clientSessionEntity); + + return clientEntityToAdapterFunc(clientSession.getRealm(), + clientSession.getClient(), offlineUserSession).apply(clientSessionEntity); + } + + @Override + public Stream getOfflineUserSessionsStream(RealmModel realm, UserModel user) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, user.getId()); + + LOG.tracef("getOfflineUserSessionsStream(%s, %s)%s", realm, user, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull); + } + + @Override + public UserSessionModel getOfflineUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.BROKER_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, brokerSessionId); + + LOG.tracef("getOfflineUserSessionByBrokerSessionId(%s, %s)%s", realm, brokerSessionId, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .findFirst() + .map(userEntityToAdapterFunc(realm)) + .orElse(null); + } + + @Override + public Stream getOfflineUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.BROKER_USER_ID, ModelCriteriaBuilder.Operator.EQ, brokerUserId); + + LOG.tracef("getOfflineUserSessionByBrokerUserIdStream(%s, %s)%s", realm, brokerUserId, getShortStackTrace()); + + return userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull); + } + + @Override + public long getOfflineSessionsCount(RealmModel realm, ClientModel client) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId()); + + LOG.tracef("getOfflineSessionsCount(%s, %s)%s", realm, client, getShortStackTrace()); + + return userSessionTx.getCount(mcb); + } + + @Override + public Stream getOfflineUserSessionsStream(RealmModel realm, ClientModel client, + Integer firstResult, Integer maxResults) { + ModelCriteriaBuilder mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId()); + + LOG.tracef("getOfflineUserSessionsStream(%s, %s, %s, %s)%s", realm, client, firstResult, maxResults, getShortStackTrace()); + + return paginatedStream(userSessionTx.getUpdatedNotRemoved(mcb) + .map(userEntityToAdapterFunc(realm)) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh)), firstResult, maxResults); + } + + @Override + public void importUserSessions(Collection persistentUserSessions, boolean offline) { + if (persistentUserSessions == null || persistentUserSessions.isEmpty()) { + return; + } + + persistentUserSessions.stream() + .map(pus -> { + MapUserSessionEntity userSessionEntity = new MapUserSessionEntity(UUID.randomUUID(), pus.getRealm(), pus.getUser(), + pus.getLoginUsername(), pus.getIpAddress(), pus.getAuthMethod(), + pus.isRememberMe(), pus.getBrokerSessionId(), pus.getBrokerUserId(), offline); + + for (Map.Entry entry : pus.getAuthenticatedClientSessions().entrySet()) { + MapAuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(entry.getValue(), entry.getValue().getUserSession(), offline); + + // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value + clientSession.setTimestamp(userSessionEntity.getLastSessionRefresh()); + + userSessionEntity.addAuthenticatedClientSession(entry.getKey(), clientSession.getId()); + + clientSessionTx.create(clientSession.getId(), clientSession); + } + + return userSessionEntity; + }) + .forEach(use -> userSessionTx.create(use.getId(), use)); + } + + @Override + public void close() { + + } + + private Stream getOfflineUserSessionEntityStream(RealmModel realm, String userSessionId) { + UUID uuid = toUUID(userSessionId); + if (uuid == null) { + return Stream.empty(); + } + + // first get a user entity by ID + ModelCriteriaBuilder mcb = userSessionStore.createCriteriaBuilder() + .compare(UserSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, uuid); + + // check if it's an offline user session + MapUserSessionEntity userSessionEntity = userSessionTx.getUpdatedNotRemoved(mcb).findFirst().orElse(null); + if (userSessionEntity != null) { + if (userSessionEntity.isOffline()) { + return Stream.of(userSessionEntity); + } + } else { + // no session found by the given ID, try to find by corresponding session ID + mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, userSessionId); + return userSessionTx.getUpdatedNotRemoved(mcb); + } + + // it's online user session so lookup offline user session by corresponding session id reference + String offlineUserSessionId = userSessionEntity.getNote(CORRESPONDING_SESSION_ID); + if (offlineUserSessionId != null) { + mcb = realmAndOfflineCriteriaBuilder(realm, true) + .compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, UUID.fromString(offlineUserSessionId)); + return userSessionTx.getUpdatedNotRemoved(mcb); + } + + return Stream.empty(); + } + + private ModelCriteriaBuilder realmAndOfflineCriteriaBuilder(RealmModel realm, boolean offline) { + return userSessionStore.createCriteriaBuilder() + .compare(UserSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(UserSessionModel.SearchableFields.IS_OFFLINE, ModelCriteriaBuilder.Operator.EQ, offline); + } + + private MapUserSessionEntity getUserSessionById(UUID id) { + MapUserSessionEntity userSessionEntity = transientUserSessions.get(id); + + if (userSessionEntity == null) { + MapUserSessionEntity userSession = userSessionTx.read(id); + return userSession != null ? registerEntityForChanges(userSession) : null; + } + return userSessionEntity; + } + + private MapUserSessionEntity createUserSessionEntityInstance(UserSessionModel userSession, boolean offline) { + MapUserSessionEntity entity = new MapUserSessionEntity(UUID.randomUUID(), userSession.getRealm().getId()); + + entity.setAuthMethod(userSession.getAuthMethod()); + entity.setBrokerSessionId(userSession.getBrokerSessionId()); + entity.setBrokerUserId(userSession.getBrokerUserId()); + entity.setIpAddress(userSession.getIpAddress()); + entity.setNotes(new ConcurrentHashMap<>(userSession.getNotes())); + entity.addNote(CORRESPONDING_SESSION_ID, userSession.getId()); + + entity.clearAuthenticatedClientSessions(); + entity.setRememberMe(userSession.isRememberMe()); + entity.setState(userSession.getState()); + entity.setLoginUsername(userSession.getLoginUsername()); + entity.setUserId(userSession.getUser().getId()); + + entity.setStarted(userSession.getStarted()); + entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + entity.setOffline(offline); + + return entity; + } + + private MapAuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession, + UserSessionModel userSession, boolean offline) { + MapAuthenticatedClientSessionEntity entity = new MapAuthenticatedClientSessionEntity(UUID.randomUUID(), + userSession.getId(), clientSession.getRealm().getId(), clientSession.getClient().getId(), offline); + + entity.setAction(clientSession.getAction()); + entity.setAuthMethod(clientSession.getProtocol()); + + entity.setNotes(new ConcurrentHashMap<>(clientSession.getNotes())); + entity.setRedirectUri(clientSession.getRedirectUri()); + entity.setTimestamp(clientSession.getTimestamp()); + + return entity; + } + + private UUID toUUID(String id) { + try { + return UUID.fromString(id); + } catch (IllegalArgumentException ex) { + return null; + } + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java new file mode 100644 index 0000000000..ff97d5dd87 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.map.common.AbstractMapProviderFactory; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; + +import java.util.UUID; + +/** + * @author Martin Kanis + */ +public class MapUserSessionProviderFactory extends AbstractMapProviderFactory + implements UserSessionProviderFactory { + + private MapStorage userSessionStore; + private MapStorage clientSessionStore; + + @Override + public void postInit(KeycloakSessionFactory factory) { + MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class); + userSessionStore = sp.getStorage("userSessions", UUID.class, MapUserSessionEntity.class, UserSessionModel.class); + clientSessionStore = sp.getStorage("clientSessions", UUID.class, MapAuthenticatedClientSessionEntity.class, AuthenticatedClientSessionModel.class); + + factory.register(event -> { + if (event instanceof UserModel.UserRemovedEvent) { + UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; + + MapUserSessionProvider provider = MapUserSessionProviderFactory.this.create(userRemovedEvent.getKeycloakSession()); + provider.removeUserSessions(userRemovedEvent.getRealm(), userRemovedEvent.getUser()); + } + }); + } + + @Override + public void loadPersistentSessions(KeycloakSessionFactory sessionFactory, int maxErrors, int sessionsPerSegment) { + + } + + @Override + public MapUserSessionProvider create(KeycloakSession session) { + return new MapUserSessionProvider(session, userSessionStore, clientSessionStore); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/SessionExpiration.java b/model/map/src/main/java/org/keycloak/models/map/userSession/SessionExpiration.java new file mode 100644 index 0000000000..5da92eb6f5 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/SessionExpiration.java @@ -0,0 +1,153 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.userSession; + +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; + +/** + * @author Martin Kanis + */ +public class SessionExpiration { + + public static void setClientSessionExpiration(MapAuthenticatedClientSessionEntity entity, RealmModel realm, ClientModel client) { + if (entity.isOffline()) { + long sessionExpires = entity.getTimestamp() + realm.getOfflineSessionIdleTimeout(); + if (realm.isOfflineSessionMaxLifespanEnabled()) { + sessionExpires = entity.getTimestamp() + realm.getOfflineSessionMaxLifespan(); + + long clientOfflineSessionMaxLifespan; + String clientOfflineSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN); + if (clientOfflineSessionMaxLifespanPerClient != null && !clientOfflineSessionMaxLifespanPerClient.trim().isEmpty()) { + clientOfflineSessionMaxLifespan = Long.parseLong(clientOfflineSessionMaxLifespanPerClient); + } else { + clientOfflineSessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan(); + } + + if (clientOfflineSessionMaxLifespan > 0) { + long clientOfflineSessionMaxExpiration = entity.getTimestamp() + clientOfflineSessionMaxLifespan; + sessionExpires = Math.min(sessionExpires, clientOfflineSessionMaxExpiration); + } + } + + long expiration = entity.getTimestamp() + realm.getOfflineSessionIdleTimeout(); + + long clientOfflineSessionIdleTimeout; + String clientOfflineSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT); + if (clientOfflineSessionIdleTimeoutPerClient != null && !clientOfflineSessionIdleTimeoutPerClient.trim().isEmpty()) { + clientOfflineSessionIdleTimeout = Long.parseLong(clientOfflineSessionIdleTimeoutPerClient); + } else { + clientOfflineSessionIdleTimeout = realm.getClientOfflineSessionIdleTimeout(); + } + + if (clientOfflineSessionIdleTimeout > 0) { + long clientOfflineSessionIdleExpiration = entity.getTimestamp() + clientOfflineSessionIdleTimeout; + expiration = Math.min(expiration, clientOfflineSessionIdleExpiration); + } + + entity.setExpiration(Math.min(expiration, sessionExpires)); + } else { + long sessionExpires = (long) entity.getTimestamp() + (realm.getSsoSessionMaxLifespanRememberMe() > 0 + ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan()); + + long clientSessionMaxLifespan; + String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN); + if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) { + clientSessionMaxLifespan = Long.parseLong(clientSessionMaxLifespanPerClient); + } else { + clientSessionMaxLifespan = realm.getClientSessionMaxLifespan(); + } + + if (clientSessionMaxLifespan > 0) { + long clientSessionMaxExpiration = entity.getTimestamp() + clientSessionMaxLifespan; + sessionExpires = Math.min(sessionExpires, clientSessionMaxExpiration); + } + + long expiration = (long) entity.getTimestamp() + (realm.getSsoSessionIdleTimeoutRememberMe() > 0 + ? realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout()); + + long clientSessionIdleTimeout; + String clientSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT); + if (clientSessionIdleTimeoutPerClient != null && !clientSessionIdleTimeoutPerClient.trim().isEmpty()) { + clientSessionIdleTimeout = Long.parseLong(clientSessionIdleTimeoutPerClient); + } else { + clientSessionIdleTimeout = realm.getClientSessionIdleTimeout(); + } + + if (clientSessionIdleTimeout > 0) { + long clientSessionIdleExpiration = entity.getTimestamp() + clientSessionIdleTimeout; + expiration = Math.min(expiration, clientSessionIdleExpiration); + } + + entity.setExpiration(Math.min(expiration, sessionExpires)); + } + } + + public static void setUserSessionExpiration(MapUserSessionEntity entity, RealmModel realm) { + if (entity.isOffline()) { + long sessionExpires = entity.getLastSessionRefresh() + realm.getOfflineSessionIdleTimeout(); + if (realm.isOfflineSessionMaxLifespanEnabled()) { + sessionExpires = entity.getStarted() + realm.getOfflineSessionMaxLifespan(); + + long clientOfflineSessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan(); + + if (clientOfflineSessionMaxLifespan > 0) { + long clientOfflineSessionMaxExpiration = entity.getStarted() + clientOfflineSessionMaxLifespan; + sessionExpires = Math.min(sessionExpires, clientOfflineSessionMaxExpiration); + } + } + + long expiration = entity.getLastSessionRefresh() + realm.getOfflineSessionIdleTimeout(); + + long clientOfflineSessionIdleTimeout = realm.getClientOfflineSessionIdleTimeout(); + + if (clientOfflineSessionIdleTimeout > 0) { + long clientOfflineSessionIdleExpiration = Time.currentTime() + clientOfflineSessionIdleTimeout; + expiration = Math.min(expiration, clientOfflineSessionIdleExpiration); + } + + entity.setExpiration(Math.min(expiration, sessionExpires)); + } else { + long sessionExpires = (long) entity.getStarted() + + (entity.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 + ? realm.getSsoSessionMaxLifespanRememberMe() + : realm.getSsoSessionMaxLifespan()); + + long clientSessionMaxLifespan = realm.getClientSessionMaxLifespan(); + + if (clientSessionMaxLifespan > 0) { + long clientSessionMaxExpiration = entity.getStarted() + clientSessionMaxLifespan; + sessionExpires = Math.min(sessionExpires, clientSessionMaxExpiration); + } + + long expiration = (long) entity.getLastSessionRefresh() + (entity.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 + ? realm.getSsoSessionIdleTimeoutRememberMe() + : realm.getSsoSessionIdleTimeout()); + + long clientSessionIdleTimeout = realm.getClientSessionIdleTimeout(); + + if (clientSessionIdleTimeout > 0) { + long clientSessionIdleExpiration = entity.getLastSessionRefresh() + clientSessionIdleTimeout; + expiration = Math.min(expiration, clientSessionIdleExpiration); + } + + entity.setExpiration(Math.min(expiration, sessionExpires)); + } + } +} diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserLoginFailureProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserLoginFailureProviderFactory new file mode 100644 index 0000000000..b45b9c19fb --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserLoginFailureProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2021 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory \ No newline at end of file diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory new file mode 100644 index 0000000000..55d75cd0fc --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserSessionProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2021 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.keycloak.models.map.userSession.MapUserSessionProviderFactory \ No newline at end of file diff --git a/server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureProviderFactory.java new file mode 100644 index 0000000000..192d934564 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Martin Kanis + */ +public interface UserLoginFailureProviderFactory extends ProviderFactory { + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureSpi.java b/server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureSpi.java new file mode 100644 index 0000000000..0405a88fee --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/UserLoginFailureSpi.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Martin Kanis + */ +public class UserLoginFailureSpi implements Spi { + + public static final String NAME = "loginFailure"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Class getProviderClass() { + return UserLoginFailureProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return UserLoginFailureProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 8b5451cfeb..5fc115c315 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -33,6 +33,7 @@ org.keycloak.models.SamlArtifactSessionMappingStoreSpi org.keycloak.models.SingleUseTokenStoreSpi org.keycloak.models.TokenRevocationStoreSpi org.keycloak.models.UserSessionSpi +org.keycloak.models.UserLoginFailureSpi org.keycloak.models.UserSpi org.keycloak.models.session.UserSessionPersisterSpi org.keycloak.models.dblock.DBLockSpi diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java index 55628cb766..a6410a9dc3 100644 --- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java @@ -21,12 +21,22 @@ package org.keycloak.models; import java.util.Map; import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.storage.SearchableModelField; /** * @author Marek Posolda */ public interface AuthenticatedClientSessionModel extends CommonClientSessionModel { + class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); + public static final SearchableModelField CLIENT_ID = new SearchableModelField<>("clientId", String.class); + public static final SearchableModelField USER_SESSION_ID = new SearchableModelField<>("userSessionId", String.class); + public static final SearchableModelField IS_OFFLINE = new SearchableModelField<>("isOffline", Boolean.class); + public static final SearchableModelField TIMESTAMP = new SearchableModelField<>("timestamp", Integer.class); + } + String getId(); int getTimestamp(); diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index c91abc75f5..a1f85419bc 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -20,7 +20,6 @@ package org.keycloak.models; import org.keycloak.component.ComponentModel; import org.keycloak.models.cache.UserCache; import org.keycloak.provider.InvalidationHandler; -import org.keycloak.provider.InvalidationHandler.InvalidableObjectType; import org.keycloak.provider.Provider; import org.keycloak.services.clientpolicy.ClientPolicyManager; import org.keycloak.sessions.AuthenticationSessionProvider; @@ -178,6 +177,14 @@ public interface KeycloakSession extends InvalidationHandler { */ UserSessionProvider sessions(); + /** + * Returns a managed provider instance. Will start a provider transaction. This transaction is managed by the KeycloakSession + * transaction. + * + * @return {@link UserLoginFailureProvider} + * @throws IllegalStateException if transaction is not active + */ + UserLoginFailureProvider loginFailures(); AuthenticationSessionProvider authenticationSessions(); diff --git a/server-spi/src/main/java/org/keycloak/models/UserLoginFailureModel.java b/server-spi/src/main/java/org/keycloak/models/UserLoginFailureModel.java index 7957ad98b4..15a69b133e 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserLoginFailureModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserLoginFailureModel.java @@ -17,12 +17,20 @@ package org.keycloak.models; +import org.keycloak.storage.SearchableModelField; + /** * @author Bill Burke * @version $Revision: 1 $ */ -public interface UserLoginFailureModel -{ +public interface UserLoginFailureModel { + + class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); + public static final SearchableModelField USER_ID = new SearchableModelField<>("userId", String.class); + } + String getUserId(); int getFailedLoginNotBefore(); void setFailedLoginNotBefore(int notBefore); diff --git a/server-spi/src/main/java/org/keycloak/models/UserLoginFailureProvider.java b/server-spi/src/main/java/org/keycloak/models/UserLoginFailureProvider.java new file mode 100644 index 0000000000..8b35e58091 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/UserLoginFailureProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models; + +import org.keycloak.provider.Provider; + +/** + * @author Martin Kanis + */ +public interface UserLoginFailureProvider extends Provider { + + /** + * Returns the {@link UserLoginFailureModel} for the given realm and user id. + * @param realm {@link RealmModel} + * @param userId {@link String} Id of the user. + * @return Returns the {@link UserLoginFailureModel} for the given realm and user id. + */ + UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId); + + /** + * Adds a {@link UserLoginFailureModel} for the given realm and user id. + * @param realm {@link RealmModel} + * @param userId {@link String} Id of the user. + * @return Returns newly created {@link UserLoginFailureModel}. + */ + UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId); + + /** + * Removes a {@link UserLoginFailureModel} for the given realm and user id. + * @param realm {@link RealmModel} + * @param userId {@link String} Id of the user. + */ + void removeUserLoginFailure(RealmModel realm, String userId); + + /** + * Removes all the {@link UserLoginFailureModel} for the given realm. + * @param realm {@link RealmModel} + */ + void removeAllUserLoginFailures(RealmModel realm); + +} diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java index 4bab579b5f..f1f58e82fd 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java @@ -17,6 +17,8 @@ package org.keycloak.models; +import org.keycloak.storage.SearchableModelField; + import java.util.Collection; import java.util.Map; @@ -25,6 +27,28 @@ import java.util.Map; */ public interface UserSessionModel { + class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + + /** + * Represents the corresponding offline user session for the online user session. + * null if there is no corresponding offline user session. + */ + public static final SearchableModelField CORRESPONDING_SESSION_ID = new SearchableModelField<>("correspondingSessionId", String.class); + public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); + public static final SearchableModelField USER_ID = new SearchableModelField<>("userId", String.class); + public static final SearchableModelField CLIENT_ID = new SearchableModelField<>("clientId", String.class); + public static final SearchableModelField BROKER_SESSION_ID = new SearchableModelField<>("brokerSessionId", String.class); + public static final SearchableModelField BROKER_USER_ID = new SearchableModelField<>("brokerUserId", String.class); + public static final SearchableModelField IS_OFFLINE = new SearchableModelField<>("isOffline", Boolean.class); + public static final SearchableModelField LAST_SESSION_REFRESH = new SearchableModelField<>("lastSessionRefresh", Integer.class); + } + + /** + * Represents the corresponding online/offline user session. + */ + String CORRESPONDING_SESSION_ID = "correspondingSessionId"; + String getId(); RealmModel getRealm(); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index 95bd857542..0ce8ccf14c 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -33,6 +33,12 @@ import java.util.stream.Stream; */ public interface UserSessionProvider extends Provider { + /** + * Returns currently used Keycloak session. + * @return {@link KeycloakSession} + */ + KeycloakSession getKeycloakSession(); + AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession); AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline); @@ -52,11 +58,11 @@ public interface UserSessionProvider extends Provider { } /** - * Obtains the user sessions associated with the specified user. + * Obtains the online user sessions associated with the specified user. * * @param realm a reference to the realm. * @param user the user whose sessions are being searched. - * @return a non-null {@link Stream} of user sessions. + * @return a non-null {@link Stream} of online user sessions. */ Stream getUserSessionsStream(RealmModel realm, UserModel user); @@ -69,11 +75,11 @@ public interface UserSessionProvider extends Provider { } /** - * Obtains the user sessions associated with the specified client. + * Obtains the online user sessions associated with the specified client. * * @param realm a reference to the realm. * @param client the client whose user sessions are being searched. - * @return a non-null {@link Stream} of user sessions. + * @return a non-null {@link Stream} of online user sessions. */ Stream getUserSessionsStream(RealmModel realm, ClientModel client); @@ -86,14 +92,14 @@ public interface UserSessionProvider extends Provider { } /** - * Obtains the user sessions associated with the specified client, starting from the {@code firstResult} and containing + * Obtains the online user sessions associated with the specified client, starting from the {@code firstResult} and containing * at most {@code maxResults}. * * @param realm a reference tot he realm. * @param client the client whose user sessions are being searched. * @param firstResult first result to return. Ignored if negative or {@code null}. * @param maxResults maximum number of results to return. Ignored if negative or {@code null}. - * @return a non-null {@link Stream} of user sessions. + * @return a non-null {@link Stream} of online user sessions. */ Stream getUserSessionsStream(RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults); @@ -107,11 +113,11 @@ public interface UserSessionProvider extends Provider { } /** - * Obtains the user sessions associated with the user that matches the specified {@code brokerUserId}. + * Obtains the online user sessions associated with the user that matches the specified {@code brokerUserId}. * * @param realm a reference to the realm. * @param brokerUserId the id of the broker user whose sessions are being searched. - * @return a non-null {@link Stream} of user sessions. + * @return a non-null {@link Stream} of online user sessions. */ Stream getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId); @@ -154,10 +160,37 @@ public interface UserSessionProvider extends Provider { void removeUserSessions(RealmModel realm); - UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId); - UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId); - void removeUserLoginFailure(RealmModel realm, String userId); - void removeAllUserLoginFailures(RealmModel realm); + /** + * @deprecated Use {@link UserLoginFailureProvider#getUserLoginFailure(RealmModel, String) getUserLoginFailure} instead. + */ + @Deprecated + default UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) { + return getKeycloakSession().loginFailures().getUserLoginFailure(realm, userId); + } + + /** + * @deprecated Use {@link UserLoginFailureProvider#addUserLoginFailure(RealmModel, String) addUserLoginFailure} instead. + */ + @Deprecated + default UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) { + return getKeycloakSession().loginFailures().addUserLoginFailure(realm, userId); + } + + /** + * @deprecated Use {@link UserLoginFailureProvider#removeUserLoginFailure(RealmModel, String) removeUserLoginFailure} instead. + */ + @Deprecated + default void removeUserLoginFailure(RealmModel realm, String userId) { + getKeycloakSession().loginFailures().removeUserLoginFailure(realm, userId); + } + + /** + * @deprecated Use {@link UserLoginFailureProvider#removeAllUserLoginFailures(RealmModel) removeAllUserLoginFailures} instead. + */ + @Deprecated + default void removeAllUserLoginFailures(RealmModel realm) { + getKeycloakSession().loginFailures().removeAllUserLoginFailures(realm); + } void onRealmRemoved(RealmModel realm); void onClientRemoved(RealmModel realm, ClientModel client); 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 ad0f8403e5..b22f991861 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -49,7 +49,6 @@ import org.keycloak.models.TokenRevocationStoreProvider; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RoleUtils; import org.keycloak.protocol.ProtocolMapperUtils; @@ -92,7 +91,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import static org.keycloak.representations.IDToken.NONCE; -import static org.keycloak.representations.IDToken.PHONE_NUMBER; /** * Stateless object that creates tokens and manages oauth access codes @@ -265,7 +263,12 @@ public class TokenManager { } if (valid) { - userSession.setLastSessionRefresh(Time.currentTime()); + int currentTime = Time.currentTime(); + userSession.setLastSessionRefresh(currentTime); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); + if (clientSession != null) { + clientSession.setTimestamp(currentTime); + } } } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 59f07f257e..09c84e4d3f 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -24,6 +24,7 @@ import org.keycloak.keys.DefaultKeyManager; import org.keycloak.models.ClientProvider; import org.keycloak.models.ClientScopeProvider; import org.keycloak.models.GroupProvider; +import org.keycloak.models.UserLoginFailureProvider; import org.keycloak.models.TokenManager; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; @@ -40,7 +41,6 @@ import org.keycloak.models.UserSessionProvider; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.provider.InvalidationHandler.InvalidableObjectType; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.services.clientpolicy.ClientPolicyManager; @@ -90,6 +90,7 @@ public class DefaultKeycloakSession implements KeycloakSession { private GroupStorageManager groupStorageManager; private UserCredentialStoreManager userCredentialStorageManager; private UserSessionProvider sessionProvider; + private UserLoginFailureProvider userLoginFailureProvider; private AuthenticationSessionProvider authenticationSessionProvider; private UserFederatedStorageProvider userFederatedStorageProvider; private KeycloakContext context; @@ -447,6 +448,14 @@ public class DefaultKeycloakSession implements KeycloakSession { return sessionProvider; } + @Override + public UserLoginFailureProvider loginFailures() { + if (userLoginFailureProvider == null) { + userLoginFailureProvider = getProvider(UserLoginFailureProvider.class); + } + return userLoginFailureProvider; + } + @Override public AuthenticationSessionProvider authenticationSessions() { if (authenticationSessionProvider == null) { diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 180649ad65..4a05737513 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -68,7 +68,6 @@ import org.keycloak.protocol.oidc.BackchannelLogoutResponse; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.protocol.saml.SamlClient; import org.keycloak.representations.AccessToken; import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; @@ -101,6 +100,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue; +import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID; import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow; import static org.keycloak.services.util.CookieHelper.getCookie; @@ -282,7 +282,11 @@ public class AuthenticationManager { new UserSessionManager(session).revokeOfflineUserSession(userSession); // Check if "online" session still exists and remove it too - UserSessionModel onlineUserSession = session.sessions().getUserSession(realm, userSession.getId()); + String onlineUserSessionId = userSession.getNote(CORRESPONDING_SESSION_ID); + UserSessionModel onlineUserSession = (onlineUserSessionId != null) ? + session.sessions().getUserSession(realm, onlineUserSessionId) : + session.sessions().getUserSession(realm, userSession.getId()); + if (onlineUserSession != null) { session.sessions().removeUserSession(realm, onlineUserSession); } diff --git a/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java b/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java index 21b03368a3..831db86bae 100644 --- a/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java +++ b/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java @@ -106,7 +106,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector UserLoginFailureModel userLoginFailure = getUserModel(session, event); if (userLoginFailure == null) { - userLoginFailure = session.sessions().addUserLoginFailure(realm, userId); + userLoginFailure = session.loginFailures().addUserLoginFailure(realm, userId); } userLoginFailure.setLastIPFailure(event.ip); long currentTime = Time.currentTimeMillis(); @@ -172,7 +172,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector protected UserLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) { RealmModel realm = getRealmModel(session, event); if (realm == null) return null; - UserLoginFailureModel user = session.sessions().getUserLoginFailure(realm, event.userId); + UserLoginFailureModel user = session.loginFailures().getUserLoginFailure(realm, event.userId); if (user == null) return null; return user; } @@ -304,7 +304,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector @Override public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user) { - UserLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, user.getId()); + UserLoginFailureModel failure = session.loginFailures().getUserLoginFailure(realm, user.getId()); if (failure != null) { int currTime = (int) (Time.currentTimeMillis() / 1000); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java index ca374c1e72..2e7a95b107 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java @@ -94,7 +94,7 @@ public class AttackDetectionResource { if (!realm.isBruteForceProtected()) return data; - UserLoginFailureModel model = session.sessions().getUserLoginFailure(realm, userId); + UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, userId); if (model == null) return data; boolean disabled; @@ -129,9 +129,9 @@ public class AttackDetectionResource { } else { auth.users().requireManage(user); } - UserLoginFailureModel model = session.sessions().getUserLoginFailure(realm, userId); + UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, userId); if (model != null) { - session.sessions().removeUserLoginFailure(realm, userId); + session.loginFailures().removeUserLoginFailure(realm, userId); adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success(); } } @@ -147,7 +147,7 @@ public class AttackDetectionResource { public void clearAllBruteForce() { auth.users().requireManage(); - session.sessions().removeAllUserLoginFailures(realm); + session.loginFailures().removeAllUserLoginFailures(realm); adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index b7db9c0567..19632587fe 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -163,7 +163,7 @@ public class UserResource { try { if (rep.isEnabled() != null && rep.isEnabled()) { - UserLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, user.getId()); + UserLoginFailureModel failureModel = session.loginFailures().getUserLoginFailure(realm, user.getId()); if (failureModel != null) { failureModel.clearFailures(); } diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java index 6db5922cc1..b4daf27e41 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java @@ -50,6 +50,7 @@ public abstract class AbstractShowTokensServlet extends HttpServlet { return new StringBuilder("" + accessTokenPretty + "") .append("" + refreshTokenPretty + "") .append("" + ctx.getTokenString() + "") + .append("" + ctx.getRefreshToken() + "") .toString(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java index 64d0de2eb7..e88aa81082 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java @@ -41,6 +41,8 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl @FindBy(id = "accessTokenString") private WebElement accessTokenString; + @FindBy(id = "refreshTokenString") + private WebElement refreshTokenString; public AccessToken getAccessToken() { try { @@ -77,4 +79,14 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl return null; } + + public String getRefreshTokenString() { + try { + return refreshTokenString.getText(); + } catch (NoSuchElementException nsee) { + log.warn("No refreshTokenString element found on the page"); + } + + return null; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java index 9eb3b347fd..d7c447236d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java @@ -26,8 +26,9 @@ public class OfflineToken extends AbstractShowTokensPage { } public void logout() { - log.info("Logging out, navigating to: " + getUriBuilder().path("/logout").build().toASCIIString()); - driver.navigate().to(getUriBuilder().path("/logout").build().toASCIIString()); + String uri = getUriBuilder().path("/logout").build().toASCIIString(); + log.info("Logging out, navigating to: " + uri); + driver.navigate().to(uri); pause(300); // this is needed for FF for some reason waitUntilElement(By.tagName("body")).is().visible(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java index f97562fb7d..32a8bb34b7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.adapter.servlet; import javax.ws.rs.core.UriBuilder; import java.util.List; + import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.graphene.page.Page; import org.jboss.shrinkwrap.api.spec.WebArchive; @@ -19,6 +20,8 @@ import org.keycloak.testsuite.adapter.page.OfflineToken; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; +import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import org.keycloak.testsuite.pages.AccountApplicationsPage; import org.keycloak.testsuite.pages.LoginPage; @@ -66,6 +69,9 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest { @Page protected OAuthGrantPage oauthGrantPage; + @Rule + public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); + private final String DEFAULT_USERNAME = "test-user@localhost"; private final String DEFAULT_PASSWORD = "password"; private final String OFFLINE_CLIENT_ID = "offline-client"; @@ -94,6 +100,8 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest { String servletUri = UriBuilder.fromUri(offlineTokenPage.toString()) .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) .build().toString(); + oauth.redirectUri(offlineTokenPage.toString()); + oauth.clientId("offline-client"); driver.navigate().to(servletUri); waitUntilElement(By.tagName("body")).is().visible(); @@ -110,17 +118,31 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest { String accessTokenId = offlineTokenPage.getAccessToken().getId(); String refreshTokenId = offlineTokenPage.getRefreshToken().getId(); + // online user session will be expired and removed setAdapterAndServerTimeOffset(9999); + + // still able to access the page using the offline token offlineTokenPage.navigateTo(); assertCurrentUrlStartsWith(offlineTokenPage); + // assert successful refresh assertThat(offlineTokenPage.getRefreshToken().getId(), not(refreshTokenId)); assertThat(offlineTokenPage.getAccessToken().getId(), not(accessTokenId)); - // Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB) - offlineTokenPage.logout(); - assertCurrentUrlDoesntStartWith(offlineTokenPage); + // logout doesn't make sense because online user session is gone and there is no KEYCLOAK_IDENTITY / KEYCLOAK_SESSION cookie in the browser + // navigate to login page which won't be possible if there's valid online session + driver.navigate().to(oauth.getLoginFormUrl()); + WaitUtils.waitForPageToLoad(); loginPage.assertCurrent(); + + // navigate back to offlineTokenPage to verify the offline session is still valid + offlineTokenPage.navigateTo(); + assertCurrentUrlStartsWith(offlineTokenPage); + + // logout the offline user session using the offline refresh token + oauth.doLogout(offlineTokenPage.getRefreshTokenString(), "secret1"); + + // can't access the offlineTokenPage anymore offlineTokenPage.navigateTo(); assertCurrentUrlDoesntStartWith(offlineTokenPage); loginPage.assertCurrent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java index fd8461ad09..0d68752a21 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java @@ -42,13 +42,17 @@ import org.apache.http.util.EntityUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.jose.jws.JWSInput; +import org.keycloak.models.UserSessionSpi; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; @@ -78,8 +82,11 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { protected static final int CLIENTS_PER_THREAD = 30; protected static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS; + private String userSessionProvider; + @Before public void beforeTest() { + userSessionProvider = testingClient.server().fetch(session -> Config.getProvider(UserSessionSpi.NAME), String.class); createClients(); } @@ -105,6 +112,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { @Test public void concurrentLoginSingleUser() throws Throwable { + Assume.assumeThat("Test runs only with InfinispanUserSessionProvider", + userSessionProvider, + Matchers.is(InfinispanUserSessionProviderFactory.PROVIDER_ID)); + log.info("*********************************************"); long start = System.currentTimeMillis(); @@ -169,6 +180,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { @Test public void concurrentLoginMultipleUsers() throws Throwable { + Assume.assumeThat("Test runs only with InfinispanUserSessionProvider", + userSessionProvider, + Matchers.is(InfinispanUserSessionProviderFactory.PROVIDER_ID)); + log.info("*********************************************"); long start = System.currentTimeMillis(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java index 13466aa70f..4c50d2da98 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java @@ -265,7 +265,7 @@ public class BruteForceCrossDCTest extends AbstractAdminCrossDCTest { private void addUserLoginFailure(KeycloakTestingClient testingClient) throws URISyntaxException, IOException { testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName(REALM_NAME); - UserLoginFailureModel loginFailure = session.sessions().addUserLoginFailure(realm, "login-test-1"); + UserLoginFailureModel loginFailure = session.loginFailures().addUserLoginFailure(realm, "login-test-1"); loginFailure.incrementFailures(); }); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java index a5ef55ef28..cae291e6c8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java @@ -28,6 +28,7 @@ import org.keycloak.component.ComponentModel; import org.keycloak.events.Details; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.cache.infinispan.ClientAdapter; import org.keycloak.representations.AccessToken; @@ -468,8 +469,11 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest { OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "password"); AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken()); + String offlineUserSessionId = testingClient.server().fetch((KeycloakSession session) -> + session.sessions().getOfflineUserSession(session.realms().getRealmByName("test"), offlineToken.getSessionState()).getId(), String.class); + Assert.assertEquals(200, response.getStatusCode()); - Assert.assertEquals(sessionId, refreshedToken.getSessionState()); + Assert.assertEquals(offlineUserSessionId, refreshedToken.getSessionState()); // Assert new refreshToken in the response String newRefreshToken = response.getRefreshToken(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java index 66f651daec..86b549ede8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java @@ -42,7 +42,6 @@ import org.keycloak.testsuite.pages.LoginPage; import java.io.Closeable; import java.io.IOException; import java.util.Collections; -import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -55,6 +54,7 @@ import org.keycloak.testsuite.auth.page.account.AccountManagement; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.WaitUtils; @@ -68,6 +68,9 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); + @Rule + public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); + @Page protected AppPage appPage; @@ -190,9 +193,8 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest { OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); String idTokenString = tokenResponse.getIdToken(); - // wait for a timeout - // setTimeOffset doesn't work because session cookie is not invalidated thus the logout flow would continue with browser logout - TimeUnit.SECONDS.sleep(3); + // expire online user session + setTimeOffset(9999); String logoutUrl = oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(idTokenString).build(); driver.navigate().to(logoutUrl); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java index b4cab0be7b..5a6696c7e0 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java @@ -29,6 +29,7 @@ import org.keycloak.models.cache.infinispan.RealmAdapter; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static org.junit.Assert.assertNotNull; @@ -111,7 +112,7 @@ public class CacheTest extends AbstractTestRealmKeycloakTest { user.setFirstName("firstName"); user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); - UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache", + UserSessionModel userSession = session.sessions().createUserSession(UUID.randomUUID().toString(), realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); user = userSession.getUser(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java deleted file mode 100644 index 3f1abd9166..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.testsuite.model; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.common.util.Time; -import org.keycloak.connections.infinispan.InfinispanConnectionProvider; -import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserManager; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; -import org.keycloak.models.UserSessionProviderFactory; -import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.services.managers.UserSessionManager; -import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; -import org.keycloak.testsuite.arquillian.annotation.ModelTest; - -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; - -/** - * @author Marek Posolda - * @author Martin Bartos - */ -@AuthServerContainerExclude(AuthServer.REMOTE) -public class UserSessionInitializerTest extends AbstractTestRealmKeycloakTest { - private final String realmName = "test"; - - @Before - public void before() { - testingClient.server().run(session -> { - RealmModel realm = session.realms().getRealm("test"); - session.users().addUser(realm, "user1").setEmail("user1@localhost"); - session.users().addUser(realm, "user2").setEmail("user2@localhost"); - }); - } - - @After - public void after() { - testingClient.server().run(session -> { - RealmModel realm = session.realms().getRealmByName("test"); - session.sessions().removeUserSessions(realm); - - UserModel user1 = session.users().getUserByUsername(realm, "user1"); - UserModel user2 = session.users().getUserByUsername(realm, "user2"); - - UserManager um = new UserManager(session); - if (user1 != null) - um.removeUser(realm, user1); - if (user2 != null) - um.removeUser(realm, user2); - }); - } - - @Test - @ModelTest - public void testUserSessionInitializer(KeycloakSession session) { - AtomicReference startedAtomic = new AtomicReference<>(); - AtomicReference origSessionsAtomic = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInit1) -> { - KeycloakSession currentSession = inheritClientConnection(session, SessionInit1); - - int started = Time.currentTime(); - startedAtomic.set(started); - - UserSessionModel[] origSessions = createSessionsInPersisterOnly(currentSession); - origSessionsAtomic.set(origSessions); - - // Load sessions from persister into infinispan/memory - UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) currentSession.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class); - userSessionFactory.loadPersistentSessions(currentSession.getKeycloakSessionFactory(), 1, 2); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInit2) -> { - KeycloakSession currentSession = inheritClientConnection(session, SessionInit2); - RealmModel realm = currentSession.realms().getRealmByName(realmName); - - int started = startedAtomic.get(); - - UserSessionModel[] origSessions = origSessionsAtomic.get(); - - // Assert sessions are in - ClientModel testApp = realm.getClientByClientId("test-app"); - ClientModel thirdparty = realm.getClientByClientId("third-party"); - - assertThat("Count of offline sesions for client 'test-app'", currentSession.sessions().getOfflineSessionsCount(realm, testApp), is((long) 3)); - assertThat("Count of offline sesions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1)); - - List loadedSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, testApp, 0, 10) - .collect(Collectors.toList()); - UserSessionProviderTest.assertSessions(loadedSessions, origSessions); - - assertSessionLoaded(loadedSessions, origSessions[0].getId(), currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party"); - assertSessionLoaded(loadedSessions, origSessions[1].getId(), currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); - assertSessionLoaded(loadedSessions, origSessions[2].getId(), currentSession.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app"); - }); - } - - @Test - @ModelTest - public void testUserSessionInitializerWithDeletingClient(KeycloakSession session) { - AtomicReference startedAtomic = new AtomicReference<>(); - AtomicReference origSessionsAtomic = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInitWithDeleting1) -> { - KeycloakSession currentSession = inheritClientConnection(session, SessionInitWithDeleting1); - - RealmModel realm = currentSession.realms().getRealmByName(realmName); - - int started = Time.currentTime(); - startedAtomic.set(started); - - origSessionsAtomic.set(createSessionsInPersisterOnly(currentSession)); - - // Delete one of the clients now. Delete it directly in DB just for the purpose of simulating the issue (normally clients should be removed through ClientManager) - ClientModel testApp = realm.getClientByClientId("test-app"); - realm.removeClient(testApp.getId()); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInitWithDeleting2) -> { - KeycloakSession currentSession = inheritClientConnection(session, SessionInitWithDeleting2); - - // Load sessions from persister into infinispan/memory - UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) currentSession.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class); - userSessionFactory.loadPersistentSessions(currentSession.getKeycloakSessionFactory(), 1, 2); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInitWithDeleting3) -> { - KeycloakSession currentSession = inheritClientConnection(session, SessionInitWithDeleting3); - RealmModel realm = currentSession.realms().getRealmByName(realmName); - - int started = startedAtomic.get(); - - UserSessionModel[] origSessions = origSessionsAtomic.get(); - - // Assert sessions are in - ClientModel thirdparty = realm.getClientByClientId("third-party"); - - assertThat("Count of offline sesions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1)); - List loadedSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10) - .collect(Collectors.toList()); - - assertThat("Size of loaded Sessions", loadedSessions.size(), is(1)); - assertSessionLoaded(loadedSessions, origSessions[0].getId(), currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "third-party"); - - // Revert client - realm.addClient("test-app"); - }); - - } - - // Create sessions in persister + infinispan, but then delete them from infinispan cache. This is to allow later testing of initializer. Return the list of "origSessions" - private UserSessionModel[] createSessionsInPersisterOnly(KeycloakSession session) { - AtomicReference origSessionsAtomic = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister1) -> { - KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister1); - - UserSessionModel[] origSessions = createSessions(currentSession); - origSessionsAtomic.set(origSessions); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister2) -> { - KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister2); - RealmModel realm = currentSession.realms().getRealmByName(realmName); - UserSessionManager sessionManager = new UserSessionManager(currentSession); - - UserSessionModel[] origSessions = origSessionsAtomic.get(); - - for (UserSessionModel origSession : origSessions) { - UserSessionModel userSession = currentSession.sessions().getUserSession(realm, origSession.getId()); - for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { - sessionManager.createOrUpdateOfflineSession(clientSession, userSession); - } - } - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister3) -> { - KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister3); - RealmModel realm = currentSession.realms().getRealmByName(realmName); - - // Delete local user cache (persisted sessions are still kept) - InfinispanUserSessionProvider userSessionProvider = (InfinispanUserSessionProvider) currentSession.getProvider(UserSessionProvider.class); - userSessionProvider.removeLocalUserSessions(realm.getId(), true); - - // Clear ispn cache to ensure initializerState is removed as well - InfinispanConnectionProvider infinispan = currentSession.getProvider(InfinispanConnectionProvider.class); - infinispan.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME).clear(); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister4) -> { - KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister4); - RealmModel realm = currentSession.realms().getRealmByName(realmName); - - ClientModel testApp = realm.getClientByClientId("test-app"); - ClientModel thirdparty = realm.getClientByClientId("third-party"); - assertThat("Count of offline sessions for client 'test-app'", currentSession.sessions().getOfflineSessionsCount(realm, testApp), is((long) 0)); - assertThat("Count of offline sessions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 0)); - }); - - return origSessionsAtomic.get(); - } - - private AuthenticatedClientSessionModel createClientSession(KeycloakSession session, ClientModel client, UserSessionModel userSession, String redirect, String state) { - RealmModel realm = session.realms().getRealmByName(realmName); - - AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); - clientSession.setRedirectUri(redirect); - if (state != null) - clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); - return clientSession; - } - - private UserSessionModel[] createSessions(KeycloakSession session) { - RealmModel realm = session.realms().getRealmByName(realmName); - - UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null); - - createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state"); - createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state"); - - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state"); - - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state"); - - return sessions; - } - - private 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"); - } - - @Override - public void configureTestRealm(RealmRepresentation testRealm) { - } -} - diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java deleted file mode 100644 index 7c93dae1e9..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java +++ /dev/null @@ -1,613 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.testsuite.model; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.common.util.Time; -import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserManager; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.session.UserSessionPersisterProvider; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.ResetTimeOffsetEvent; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.services.managers.ClientManager; -import org.keycloak.services.managers.RealmManager; -import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; -import org.keycloak.testsuite.arquillian.annotation.ModelTest; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import org.keycloak.models.Constants; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; - -/** - * @author Marek Posolda - */ -@AuthServerContainerExclude(AuthServer.REMOTE) -public class UserSessionPersisterProviderTest extends AbstractTestRealmKeycloakTest { - - @Before - public void before() { - testingClient.server().run(session -> { - initStuff(session); - }); - } - - public static void initStuff(KeycloakSession session) { - RealmModel realm = session.realms().getRealm("test"); - session.users().addUser(realm, "user1").setEmail("user1@localhost"); - session.users().addUser(realm, "user2").setEmail("user2@localhost"); - } - - @After - public void after() { - testingClient.server().run(session -> { - RealmModel realm = session.realms().getRealm("test"); - session.sessions().removeUserSessions(realm); - UserModel user1 = session.users().getUserByUsername(realm, "user1"); - UserModel user2 = session.users().getUserByUsername(realm, "user2"); - - UserManager um = new UserManager(session); - if (user1 != null) { - um.removeUser(realm, user1); - } - if (user2 != null) { - um.removeUser(realm, user2); - } - }); - } - - @Test - @ModelTest - public void testPersistenceWithLoad(KeycloakSession session) { - int started = Time.currentTime(); - UserSessionModel[][] origSessions = new UserSessionModel[1][1]; - final UserSessionModel[] userSession = new UserSessionModel[1]; - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL) -> { - // Create some sessions in infinispan - origSessions[0] = createSessions(sessionWL); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL22) -> { - // Persist 3 created userSessions and clientSessions as offline - RealmModel realm = sessionWL22.realms().getRealm("test"); - ClientModel testApp = realm.getClientByClientId("test-app"); - sessionWL22.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList()) - .forEach(userSessionLooper -> persistUserSession(sessionWL22, userSessionLooper, true)); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL2) -> { - // Persist 1 online session - RealmModel realm = sessionWL2.realms().getRealm("test"); - userSession[0] = sessionWL2.sessions().getUserSession(realm, origSessions[0][0].getId()); - persistUserSession(sessionWL2, userSession[0], false); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL3) -> {// Assert online session - RealmModel realm = sessionWL3.realms().getRealm("test"); - List loadedSessions = loadPersistedSessionsPaginated(sessionWL3, false, 1, 1, 1); - UserSessionProviderTest.assertSession(loadedSessions.get(0), sessionWL3.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party"); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL4) -> { - // Assert offline sessions - RealmModel realm = sessionWL4.realms().getRealm("test"); - List loadedSessions = loadPersistedSessionsPaginated(sessionWL4, true, 2, 2, 3); - UserSessionProviderTest.assertSessions(loadedSessions, origSessions[0]); - - assertSessionLoaded(loadedSessions, origSessions[0][0].getId(), sessionWL4.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party"); - assertSessionLoaded(loadedSessions, origSessions[0][1].getId(), sessionWL4.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); - assertSessionLoaded(loadedSessions, origSessions[0][2].getId(), sessionWL4.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app"); - }); - } - - @Test - @ModelTest - public void testUpdateAndRemove(KeycloakSession session) { - int started = Time.currentTime(); - - AtomicReference origSessionsAt = new AtomicReference<>(); - AtomicReference> loadedSessionsAt = new AtomicReference<>(); - - AtomicReference userSessionAt = new AtomicReference<>(); - AtomicReference persistedSessionAt = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove1) -> { - KeycloakSession currentSession = sesUpdateRemove1; - - // Create some sessions in infinispan - UserSessionModel[] origSessions = createSessions(currentSession); - origSessionsAt.set(origSessions); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove2) -> { - KeycloakSession currentSession = sesUpdateRemove2; - RealmModel realm = currentSession.realms().getRealm("test"); - UserSessionModel[] origSessions = origSessionsAt.get(); - - // Persist 1 offline session - UserSessionModel userSession = currentSession.sessions().getUserSession(realm, origSessions[1].getId()); - userSessionAt.set(userSession); - - persistUserSession(currentSession, userSession, true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove3) -> { - KeycloakSession currentSession = sesUpdateRemove3; - RealmModel realm = currentSession.realms().getRealm("test"); - - UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class); - - // Load offline session - List loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1); - loadedSessionsAt.set(loadedSessions); - - UserSessionModel persistedSession = loadedSessions.get(0); - persistedSessionAt.set(persistedSession); - - UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); - - // create new clientSession - AuthenticatedClientSessionModel clientSession = createClientSession(currentSession, realm.getClientByClientId("third-party"), currentSession.sessions().getUserSession(realm, persistedSession.getId()), - "http://redirect", "state"); - persister.createClientSession(clientSession, true); - - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove4) -> { - KeycloakSession currentSession = sesUpdateRemove4; - RealmModel realm = currentSession.realms().getRealm("test"); - - UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class); - UserSessionModel userSession = userSessionAt.get(); - - // Remove clientSession - persister.removeClientSession(userSession.getId(), realm.getClientByClientId("third-party").getId(), true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove5) -> { - KeycloakSession currentSession = sesUpdateRemove5; - RealmModel realm = currentSession.realms().getRealm("test"); - - UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class); - List loadedSessions = loadedSessionsAt.get(); - UserSessionModel persistedSession = persistedSessionAt.get(); - - // Assert clientSession removed - loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1); - persistedSession = loadedSessions.get(0); - UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); - - // Remove userSession - persister.removeUserSession(persistedSession.getId(), true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove6) -> { - KeycloakSession currentSession = sesUpdateRemove6; - // Assert nothing found - loadPersistedSessionsPaginated(currentSession, true, 10, 0, 0); - }); - } - - @Test - @ModelTest - public void testOnRealmRemoved(KeycloakSession session) { - AtomicReference userSessionID = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR1) -> { - KeycloakSession currentSession = sessionRR1; - RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo"); - fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName())); - - fooRealm.addClient("foo-app"); - currentSession.users().addUser(fooRealm, "user3"); - - UserSessionModel userSession = currentSession.sessions().createUserSession(fooRealm, currentSession.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null); - userSessionID.set(userSession.getId()); - - createClientSession(currentSession, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR2) -> { - KeycloakSession currentSession = sessionRR2; - - // Persist offline session - RealmModel fooRealm = currentSession.realms().getRealm("foo"); - UserSessionModel userSession = currentSession.sessions().getUserSession(fooRealm, userSessionID.get()); - persistUserSession(currentSession, userSession, true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR3) -> { - KeycloakSession currentSession = sessionRR3; - - // Assert session was persisted - loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1); - - // Remove realm - RealmManager realmMgr = new RealmManager(currentSession); - realmMgr.removeRealm(realmMgr.getRealm("foo")); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR4) -> { - KeycloakSession currentSession = sessionRR4; - - // Assert nothing loaded - loadPersistedSessionsPaginated(currentSession, true, 10, 0, 0); - }); - } - - @Test - @ModelTest - public void testOnClientRemoved(KeycloakSession session) { - int started = Time.currentTime(); - AtomicReference userSessionID = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR1) -> { - KeycloakSession currentSession = sessionCR1; - RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo"); - fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX)); - - fooRealm.addClient("foo-app"); - fooRealm.addClient("bar-app"); - currentSession.users().addUser(fooRealm, "user3"); - - UserSessionModel userSession = currentSession.sessions().createUserSession(fooRealm, currentSession.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null); - userSessionID.set(userSession.getId()); - - createClientSession(currentSession, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); - createClientSession(currentSession, fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state"); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR2) -> { - KeycloakSession currentSession = sessionCR2; - RealmModel fooRealm = currentSession.realms().getRealm("foo"); - - // Persist offline session - UserSessionModel userSession = currentSession.sessions().getUserSession(fooRealm, userSessionID.get()); - persistUserSession(currentSession, userSession, true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR3) -> { - KeycloakSession currentSession = sessionCR3; - - RealmManager realmMgr = new RealmManager(currentSession); - ClientManager clientMgr = new ClientManager(realmMgr); - RealmModel fooRealm = realmMgr.getRealm("foo"); - - // Assert session was persisted with both clientSessions - UserSessionModel persistedSession = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1).get(0); - UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "foo-app", "bar-app"); - - // Remove foo-app client - ClientModel client = fooRealm.getClientByClientId("foo-app"); - clientMgr.removeClient(fooRealm, client); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR4) -> { - KeycloakSession currentSession = sessionCR4; - RealmManager realmMgr = new RealmManager(currentSession); - ClientManager clientMgr = new ClientManager(realmMgr); - RealmModel fooRealm = realmMgr.getRealm("foo"); - - // Assert just one bar-app clientSession persisted now - UserSessionModel persistedSession = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1).get(0); - UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "bar-app"); - - // Remove bar-app client - ClientModel client = fooRealm.getClientByClientId("bar-app"); - clientMgr.removeClient(fooRealm, client); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR5) -> { - KeycloakSession currentSession = sessionCR5; - - // Assert loading still works - last userSession is still there, but no clientSession on it - loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1); - - // Cleanup - RealmManager realmMgr = new RealmManager(currentSession); - realmMgr.removeRealm(realmMgr.getRealm("foo")); - }); - } - - @Test - @ModelTest - public void testOnUserRemoved(KeycloakSession session) { - int started = Time.currentTime(); - AtomicReference origSessionsAt = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR1) -> { - KeycloakSession currentSession = sessionOR1; - - // Create some sessions in infinispan - UserSessionModel[] origSessions = createSessions(currentSession); - origSessionsAt.set(origSessions); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR2) -> { - KeycloakSession currentSession = sessionOR2; - RealmModel realm = currentSession.realms().getRealm("test"); - - UserSessionModel[] origSessions = origSessionsAt.get(); - - // Persist 2 offline sessions of 2 users - UserSessionModel userSession1 = currentSession.sessions().getUserSession(realm, origSessions[1].getId()); - UserSessionModel userSession2 = currentSession.sessions().getUserSession(realm, origSessions[2].getId()); - persistUserSession(currentSession, userSession1, true); - persistUserSession(currentSession, userSession2, true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR3) -> { - KeycloakSession currentSession = sessionOR3; - RealmModel realm = currentSession.realms().getRealm("test"); - - // Load offline sessions - loadPersistedSessionsPaginated(currentSession, true, 10, 1, 2); - - // Properly delete user and assert his offlineSession removed - UserModel user1 = currentSession.users().getUserByUsername(realm, "user1"); - new UserManager(currentSession).removeUser(realm, user1); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR4) -> { - KeycloakSession currentSession = sessionOR4; - RealmModel realm = currentSession.realms().getRealm("test"); - - UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class); - Assert.assertEquals(1, persister.getUserSessionsCount(true)); - - List loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1); - UserSessionModel persistedSession = loadedSessions.get(0); - UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app"); - - // KEYCLOAK-2431 Assert that userSessionPersister is resistent even to situation, when users are deleted "directly". - // No exception will happen. However session will be still there - UserModel user2 = currentSession.users().getUserByUsername(realm, "user2"); - currentSession.users().removeUser(realm, user2); - - loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1); - - // Cleanup - UserSessionModel userSession = loadedSessions.get(0); - currentSession.sessions().removeUserSession(realm, userSession); - persister.removeUserSession(userSession.getId(), userSession.isOffline()); - }); - } - - // KEYCLOAK-1999 - @Test - @ModelTest - public void testNoSessions(KeycloakSession session) { - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionNS) -> { - UserSessionPersisterProvider persister = sessionNS.getProvider(UserSessionPersisterProvider.class); - Stream sessions = persister.loadUserSessionsStream(0, 1, true, 0, "abc"); - Assert.assertEquals(0, sessions.count()); - }); - } - - @Test - @ModelTest - public void testMoreSessions(KeycloakSession session) { - AtomicReference> userSessionsAt = new AtomicReference<>(); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionMS1) -> { - KeycloakSession currentSession = sessionMS1; - RealmModel realm = currentSession.realms().getRealm("test"); - - // Create 10 userSessions - each having 1 clientSession - List userSessions = new ArrayList<>(); - UserModel user = currentSession.users().getUserByUsername(realm, "user1"); - - for (int i = 0; i < 20; i++) { - // Having different offsets for each session (to ensure that lastSessionRefresh is also different) - Time.setOffset(i); - - UserSessionModel userSession = currentSession.sessions().createUserSession(realm, user, "user1", "127.0.0.1", "form", true, null, null); - createClientSession(currentSession, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state"); - userSessions.add(userSession); - } - - userSessionsAt.set(userSessions); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionMS2) -> { - KeycloakSession currentSession = sessionMS2; - RealmModel realm = currentSession.realms().getRealm("test"); - - List userSessions = userSessionsAt.get(); - - for (UserSessionModel userSession : userSessions) { - UserSessionModel userSession2 = currentSession.sessions().getUserSession(realm, userSession.getId()); - persistUserSession(currentSession, userSession2, true); - } - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionMS3) -> { - KeycloakSession currentSession = sessionMS3; - RealmModel realm = currentSession.realms().getRealm("test"); - - List loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 2, 10, 20); - UserModel user = currentSession.users().getUserByUsername(realm, "user1"); - ClientModel testApp = realm.getClientByClientId("test-app"); - - for (UserSessionModel loadedSession : loadedSessions) { - assertEquals(user.getId(), loadedSession.getUser().getId()); - assertEquals("127.0.0.1", loadedSession.getIpAddress()); - assertEquals(user.getUsername(), loadedSession.getLoginUsername()); - - assertEquals(1, loadedSession.getAuthenticatedClientSessions().size()); - assertTrue(loadedSession.getAuthenticatedClientSessions().containsKey(testApp.getId())); - } - }); - } - - @Test - @ModelTest - public void testExpiredSessions(KeycloakSession session) { - UserSessionModel[][] origSessions = {new UserSessionModel[1]}; - int started = Time.currentTime(); - final UserSessionModel[] userSession1 = {null}; - final UserSessionModel[] userSession2 = {null}; - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionES) -> { - // Create some sessions in infinispan - UserSessionPersisterProvider persister = sessionES.getProvider(UserSessionPersisterProvider.class); - origSessions[0] = createSessions(sessionES); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionES2) -> { - // Persist 2 offline sessions of 2 users - RealmModel realm = sessionES2.realms().getRealm("test"); - UserSessionPersisterProvider persister = sessionES2.getProvider(UserSessionPersisterProvider.class); - userSession1[0] = sessionES2.sessions().getUserSession(realm, origSessions[0][1].getId()); - userSession2[0] = sessionES2.sessions().getUserSession(realm, origSessions[0][2].getId()); - persistUserSession(sessionES2, userSession1[0], true); - persistUserSession(sessionES2, userSession2[0], true); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionES3) -> { - // Update one of the sessions with lastSessionRefresh of 20 days ahead - int lastSessionRefresh = Time.currentTime() + 1728000; - RealmModel realm = sessionES3.realms().getRealm("test"); - UserSessionPersisterProvider persister = sessionES3.getProvider(UserSessionPersisterProvider.class); - - persister.updateLastSessionRefreshes(realm, lastSessionRefresh, Collections.singleton(userSession1[0].getId()), true); - - // Increase time offset - 40 days - Time.setOffset(3456000); - try { - // Run expiration thread - persister.removeExpired(realm); - - // Test the updated session is still in persister. Not updated session is not there anymore - List loadedSessions = loadPersistedSessionsPaginated(sessionES3, true, 10, 1, 1); - UserSessionModel persistedSession = loadedSessions.get(0); - UserSessionProviderTest.assertSession(persistedSession, sessionES3.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, lastSessionRefresh, "test-app"); - - } finally { - // Cleanup - Time.setOffset(0); - session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent()); - } - }); - } - - private AuthenticatedClientSessionModel createClientSession(KeycloakSession session, ClientModel client, UserSessionModel userSession, String redirect, String state) { - RealmModel realm = session.realms().getRealm("test"); - AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); - clientSession.setRedirectUri(redirect); - if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); - return clientSession; - } - - private UserSessionModel[] createSessions(KeycloakSession session) { - RealmModel realm = session.realms().getRealm("test"); - UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null); - - createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state"); - createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state"); - - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state"); - - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state"); - - return sessions; - } - - private void persistUserSession(KeycloakSession session, UserSessionModel userSession, boolean offline) { - UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - persister.createUserSession(userSession, offline); - for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { - persister.createClientSession(clientSession, offline); - } - } - - 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(KeycloakSession session, boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) { - UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - - int count = persister.getUserSessionsCount(offline); - - int pageCount = 0; - boolean next = true; - List result = new ArrayList<>(); - int lastCreatedOn = 0; - String lastSessionId = "abc"; - - while (next) { - List sess = persister - .loadUserSessionsStream(0, sessionsPerPage, offline, lastCreatedOn, lastSessionId) - .collect(Collectors.toList()); - - if (sess.size() < sessionsPerPage) { - next = false; - - // We had at least some session - if (sess.size() > 0) { - pageCount++; - } - } else { - pageCount++; - - UserSessionModel lastSession = sess.get(sess.size() - 1); - lastCreatedOn = lastSession.getStarted(); - lastSessionId = lastSession.getId(); - } - - result.addAll(sess); - } - - Assert.assertEquals(expectedPageCount, pageCount); - Assert.assertEquals(expectedSessionsCount, result.size()); - return result; - } - - @Override - public void configureTestRealm(RealmRepresentation testRealm) { - } -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index 39c0d7caf6..e628a63ae7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -20,7 +20,6 @@ package org.keycloak.testsuite.model; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; @@ -30,10 +29,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserManager; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.session.UserSessionPersisterProvider; -import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.ClientManager; @@ -42,8 +38,6 @@ import org.keycloak.services.managers.UserSessionManager; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.ModelTest; -import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; -import org.keycloak.timer.TimerProvider; import java.util.HashMap; import java.util.HashSet; @@ -64,20 +58,15 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A @AuthServerContainerExclude(AuthServer.REMOTE) public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest { - @Rule - public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); - private static KeycloakSession currentSession; private static RealmModel realm; private static UserSessionManager sessionManager; - private static UserSessionPersisterProvider persister; @Before public void before() { testingClient.server().run(session -> { KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionBefore) -> { reloadState(sessionBefore, true); - persister = sessionBefore.getProvider(UserSessionPersisterProvider.class); }); }); } @@ -115,7 +104,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes currentSession = sessionCrud2; realm = currentSession.realms().getRealm("test"); sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); // Key is userSession ID, values are client UUIDS // Persist 3 created userSessions and clientSessions as offline @@ -128,7 +116,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes currentSession = sessionCrud3; realm = currentSession.realms().getRealm("test"); sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); // Assert all previously saved offline sessions found for (Map.Entry> entry : offlineSessions.entrySet()) { @@ -165,17 +152,10 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes currentSession = sessionCrud4; realm = currentSession.realms().getRealm("test"); sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); // Assert userSession revoked - ClientModel testApp = realm.getClientByClientId("test-app"); ClientModel thirdparty = realm.getClientByClientId("third-party"); - // Still 2 sessions. The count of sessions by client may not be accurate after revoke due the - // performance optimizations (the "127.0.0.1" currentSession still has another client "thirdparty" in it) - Assert.assertEquals(2, currentSession.sessions().getOfflineSessionsCount(realm, testApp)); - Assert.assertEquals(1, currentSession.sessions().getOfflineSessionsCount(realm, thirdparty)); - List thirdpartySessions = currentSession.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10) .collect(Collectors.toList()); Assert.assertEquals(1, thirdpartySessions.size()); @@ -201,7 +181,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes currentSession = sessionCrud5; realm = currentSession.realms().getRealm("test"); sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); ClientModel testApp = realm.getClientByClientId("test-app"); ClientModel thirdparty = realm.getClientByClientId("third-party"); @@ -231,9 +210,12 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR1) -> { currentSession = sessionRR1; - persister = currentSession.getProvider(UserSessionPersisterProvider.class); RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo"); fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName())); + fooRealm.setSsoSessionIdleTimeout(1800); + fooRealm.setSsoSessionMaxLifespan(36000); + fooRealm.setOfflineSessionIdleTimeout(2592000); + fooRealm.setOfflineSessionMaxLifespan(5184000); fooRealm.addClient("foo-app"); currentSession.users().addUser(fooRealm, "user3"); @@ -296,9 +278,12 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR1) -> { currentSession = sessionCR1; sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo"); fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName())); + fooRealm.setSsoSessionIdleTimeout(1800); + fooRealm.setSsoSessionMaxLifespan(36000); + fooRealm.setOfflineSessionIdleTimeout(2592000); + fooRealm.setOfflineSessionMaxLifespan(5184000); fooRealm.addClient("foo-app"); fooRealm.addClient("bar-app"); @@ -392,6 +377,10 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes currentSession = sessionUR1; RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo"); fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName())); + fooRealm.setSsoSessionIdleTimeout(1800); + fooRealm.setSsoSessionMaxLifespan(36000); + fooRealm.setOfflineSessionIdleTimeout(2592000); + fooRealm.setOfflineSessionMaxLifespan(5184000); fooRealm.addClient("foo-app"); currentSession.users().addUser(fooRealm, "user3"); @@ -443,141 +432,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes }); } - @Test - @ModelTest - public void testExpired(KeycloakSession session) { - // Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider - TimerProvider timer = session.getProvider(TimerProvider.class); - TimerProvider.TimerTaskContext timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); - log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); - - try { - AtomicReference origSessionsAt = new AtomicReference<>(); - - // Key is userSessionId, value is set of client UUIDS - Map> offlineSessions = new HashMap<>(); - ClientModel[] testApp = new ClientModel[1]; - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired1) -> { - // Create some online sessions in infinispan - currentSession = sessionExpired1; - reloadState(currentSession); - UserSessionModel[] origSessions = createSessions(currentSession); - origSessionsAt.set(origSessions); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired2) -> { - currentSession = sessionExpired2; - realm = currentSession.realms().getRealm("test"); - sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); - - // Persist 3 created userSessions and clientSessions as offline - testApp[0] = realm.getClientByClientId("test-app"); - currentSession.sessions().getUserSessionsStream(realm, testApp[0]).collect(Collectors.toList()) - .forEach(userSession -> offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(currentSession, userSession))); - - // Assert all previously saved offline sessions found - for (Map.Entry> entry : offlineSessions.entrySet()) { - UserSessionModel foundSession = sessionManager.findOfflineUserSession(realm, entry.getKey()); - Assert.assertEquals(foundSession.getAuthenticatedClientSessions().keySet(), entry.getValue()); - } - }); - - log.info("Persisted 3 sessions to UserSessionPersisterProvider"); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired3) -> { - currentSession = sessionExpired3; - realm = currentSession.realms().getRealm("test"); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); - UserSessionModel[] origSessions = origSessionsAt.get(); - - UserSessionModel session0 = currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId()); - Assert.assertNotNull(session0); - - // sessions are in persister too - Assert.assertEquals(3, persister.getUserSessionsCount(true)); - - Time.setOffset(300); - log.infof("Set time offset to 300. Time is: %d", Time.currentTime()); - - // Set lastSessionRefresh to currentSession[0] to 0 - session0.setLastSessionRefresh(Time.currentTime()); - }); - - - // Increase timeOffset and update LSR of the session two times - first to 20 days and then to 21 days. At least one of updates - // will propagate to PersisterLastSessionRefreshStore and update DB (Single update is not 100% sure as there is still a - // chance of delayed periodic task to be run in the meantime and causing race-condition, which would mean LSR not updated in the DB) - for (int i=0 ; i<2 ; i++) { - int timeOffset = 1728000 + (i * 86400); - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired4) -> { - currentSession = sessionExpired4; - realm = currentSession.realms().getRealm("test"); - UserSessionModel[] origSessions = origSessionsAt.get(); - Time.setOffset(timeOffset); - log.infof("Set time offset to %d. Time is: %d", timeOffset, Time.currentTime()); - - UserSessionModel session0 = currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId()); - session0.setLastSessionRefresh(Time.currentTime()); - }); - } - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired5) -> { - currentSession = sessionExpired5; - realm = currentSession.realms().getRealm("test"); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); - - // Increase timeOffset - 40 days - Time.setOffset(3456000); - log.infof("Set time offset to 3456000. Time is: %d", Time.currentTime()); - - // Expire and ensure that all sessions despite session0 were removed - currentSession.sessions().removeExpired(realm); - persister.removeExpired(realm); - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired6) -> { - currentSession = sessionExpired6; - realm = currentSession.realms().getRealm("test"); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); - UserSessionModel[] origSessions = origSessionsAt.get(); - - // assert session0 is the only session found - Assert.assertNotNull(currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId())); - Assert.assertNull(currentSession.sessions().getOfflineUserSession(realm, origSessions[1].getId())); - Assert.assertNull(currentSession.sessions().getOfflineUserSession(realm, origSessions[2].getId())); - - Assert.assertEquals(1, persister.getUserSessionsCount(true)); - - // Expire everything and assert nothing found - Time.setOffset(7000000); - - currentSession.sessions().removeExpired(realm); - persister.removeExpired(realm); - - }); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired7) -> { - currentSession = sessionExpired7; - realm = currentSession.realms().getRealm("test"); - sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); - - for (String userSessionId : offlineSessions.keySet()) { - Assert.assertNull(sessionManager.findOfflineUserSession(realm, userSessionId)); - } - Assert.assertEquals(0, persister.getUserSessionsCount(true)); - }); - - } finally { - Time.setOffset(0); - session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent()); - timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); - } - } - - private static Set createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel userSession) { Set offlineSessions = new HashSet<>(); @@ -655,7 +509,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes currentSession.users().addUser(realm, "user2").setEmail("user2@localhost"); } sessionManager = new UserSessionManager(currentSession); - persister = currentSession.getProvider(UserSessionPersisterProvider.class); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index 6c94f5755e..e208397cb8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -26,7 +26,6 @@ import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserManager; @@ -47,12 +46,11 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -121,9 +119,10 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { public void testUpdateSession(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); UserSessionModel[] sessions = createSessions(session); - session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(1000); + int lastRefresh = Time.currentTime(); + session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(lastRefresh); - assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); + assertEquals(lastRefresh, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); } @Test @@ -131,8 +130,9 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { public void testUpdateSessionInSameTransaction(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); UserSessionModel[] sessions = createSessions(session); - session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(1000); - assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); + int lastRefresh = Time.currentTime(); + session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(lastRefresh); + assertEquals(lastRefresh, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); } @Test @@ -249,13 +249,6 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { RealmModel realm = session.realms().getRealmByName("test"); UserSessionModel[] sessions = createSessions(session); - KeycloakTransaction transaction = session.getTransactionManager(); - if (!transaction.getRollbackOnly()) { - transaction.commit(); - - } - - assertSessions(session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user1")) .collect(Collectors.toList()), sessions[0], sessions[1]); assertSessions(session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user2")) @@ -266,11 +259,8 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { @ModelTest public void testRemoveUserSessionsByUser(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); + createSessions(session); - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> { - inheritClientConnection(session, kcSession); - createSessions(kcSession); - }); Map clientSessionsKept = session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user2")) .collect(Collectors.toMap(model -> model.getId(), model -> model.getAuthenticatedClientSessions().keySet().size())); @@ -307,10 +297,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { @ModelTest public void testRemoveUserSessionsByRealm(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> { - inheritClientConnection(session, kcSession); - createSessions(kcSession); - }); + createSessions(session); KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> { kcSession.sessions().removeUserSessions(realm); @@ -411,12 +398,13 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { public void testTransientUserSession(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); ClientModel client = realm.getClientByClientId("test-app"); + String userSessionId = UUID.randomUUID().toString(); // create an user session, but don't persist it to infinispan KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession session1) -> { long sessionsBefore = session1.sessions().getActiveUserSessions(realm, client); - UserSessionModel userSession = session1.sessions().createUserSession("123", realm, session1.users().getUserByUsername(realm, "user1"), + UserSessionModel userSession = session1.sessions().createUserSession(userSessionId, realm, session1.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT); AuthenticatedClientSessionModel clientSession = session1.sessions().createClientSession(realm, client, userSession); assertEquals(userSession, clientSession.getUserSession()); @@ -424,7 +412,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { assertSession(userSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.1", userSession.getStarted(), userSession.getStarted(), "test-app"); // Can find session by ID in current transaction - UserSessionModel foundSession = session1.sessions().getUserSession(realm, "123"); + UserSessionModel foundSession = session1.sessions().getUserSession(realm, userSessionId); Assert.assertEquals(userSession, foundSession); // Count of sessions should be still the same @@ -433,7 +421,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { // create an user session whose last refresh exceeds the max session idle timeout. KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession session1) -> { - UserSessionModel userSession = session1.sessions().getUserSession(realm, "123"); + UserSessionModel userSession = session1.sessions().getUserSession(realm, userSessionId); Assert.assertNull(userSession); }); } @@ -548,12 +536,6 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { RealmModel realm = session.realms().getRealmByName("test"); UserSessionModel[] sessions = createSessions(session); - KeycloakTransaction transaction = session.getTransactionManager(); - if (!transaction.getRollbackOnly()) { - transaction.commit(); - } - - assertSessions(session.sessions().getUserSessionsStream(realm, realm.getClientByClientId("test-app")) .collect(Collectors.toList()), sessions[0], sessions[1], sessions[2]); assertSessions(session.sessions().getUserSessionsStream(realm, realm.getClientByClientId("third-party")) @@ -564,25 +546,23 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { @ModelTest public void testGetByClientPaginated(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); - try { - for (int i = 0; i < 25; i++) { - Time.setOffset(i); - UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0." + i, "form", false, null, null); - AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession); - assertNotNull(clientSession); - clientSession.setRedirectUri("http://redirect"); - clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state"); - clientSession.setTimestamp(userSession.getStarted()); - userSession.setLastSessionRefresh(userSession.getStarted()); - } - } finally { - Time.setOffset(0); - } - KeycloakTransaction transaction = session.getTransactionManager(); - if (!transaction.getRollbackOnly()) { - transaction.commit(); - } + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> { + try { + for (int i = 0; i < 25; i++) { + Time.setOffset(i); + UserSessionModel userSession = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user1"), "user1", "127.0.0." + i, "form", false, null, null); + AuthenticatedClientSessionModel clientSession = kcSession.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession); + assertNotNull(clientSession); + clientSession.setRedirectUri("http://redirect"); + clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state"); + clientSession.setTimestamp(userSession.getStarted()); + userSession.setLastSessionRefresh(userSession.getStarted()); + } + } finally { + Time.setOffset(0); + } + }); assertPaginatedSession(session, realm, realm.getClientByClientId("test-app"), 0, 1, 1); assertPaginatedSession(session, realm, realm.getClientByClientId("test-app"), 0, 10, 10); @@ -612,6 +592,8 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { @ModelTest public void testAuthenticatedClientSessions(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); + realm.setSsoSessionIdleTimeout(1800); + realm.setSsoSessionMaxLifespan(36000); UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); ClientModel client1 = realm.getClientByClientId("test-app"); @@ -620,19 +602,21 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { // Create client1 session AuthenticatedClientSessionModel clientSession1 = session.sessions().createClientSession(realm, client1, userSession); clientSession1.setAction("foo1"); - clientSession1.setTimestamp(100); + int currentTime1 = Time.currentTime(); + clientSession1.setTimestamp(currentTime1); // Create client2 session AuthenticatedClientSessionModel clientSession2 = session.sessions().createClientSession(realm, client2, userSession); clientSession2.setAction("foo2"); - clientSession2.setTimestamp(200); + int currentTime2 = Time.currentTime(); + clientSession2.setTimestamp(currentTime2); // Ensure sessions are here userSession = session.sessions().getUserSession(realm, userSession.getId()); Map clientSessions = userSession.getAuthenticatedClientSessions(); Assert.assertEquals(2, clientSessions.size()); - testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1", 100); - testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2", 200); + testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1", currentTime1); + testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2", currentTime2); // Update session1 clientSessions.get(client1.getId()).setAction("foo1-updated"); @@ -641,20 +625,21 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { // Ensure updated userSession = session.sessions().getUserSession(realm, userSession.getId()); clientSessions = userSession.getAuthenticatedClientSessions(); - testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100); + testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", currentTime1); // Rewrite session2 clientSession2 = session.sessions().createClientSession(realm, client2, userSession); clientSession2.setAction("foo2-rewrited"); - clientSession2.setTimestamp(300); + int currentTime3 = Time.currentTime(); + clientSession2.setTimestamp(currentTime3); // Ensure updated userSession = session.sessions().getUserSession(realm, userSession.getId()); clientSessions = userSession.getAuthenticatedClientSessions(); Assert.assertEquals(2, clientSessions.size()); - testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100); - testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2-rewrited", 300); + testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", currentTime1); + testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2-rewrited", currentTime3); // remove session clientSession1 = userSession.getAuthenticatedClientSessions().get(client1.getId()); @@ -675,19 +660,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { } private static void assertPaginatedSession(KeycloakSession session, RealmModel realm, ClientModel client, int start, int max, int expectedSize) { - List sessions = session.sessions().getUserSessionsStream(realm, client, start, max).collect(Collectors.toList()); - String[] actualIps = new String[sessions.size()]; - - for (int i = 0; i < actualIps.length; i++) { - actualIps[i] = sessions.get(i).getIpAddress(); - } - - String[] expectedIps = new String[expectedSize]; - for (int i = 0; i < expectedSize; i++) { - expectedIps[i] = "127.0.0." + (i + start); - } - - assertArrayEquals(expectedIps, actualIps); + assertEquals(expectedSize, session.sessions().getUserSessionsStream(realm, client, start, max).count()); } @Test @@ -698,60 +671,61 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { RealmModel realm = session.realms().getRealmByName("test"); createSessions(session); - KeycloakTransaction transaction = session.getTransactionManager(); - if (!transaction.getRollbackOnly()) { - transaction.commit(); - } - assertEquals(3, session.sessions().getActiveUserSessions(realm, realm.getClientByClientId("test-app"))); assertEquals(1, session.sessions().getActiveUserSessions(realm, realm.getClientByClientId("third-party"))); } @Test public void loginFailures() { - testingClient.server().run(UserSessionProviderTest::loginFailures); - } - public static void loginFailures(KeycloakSession session) { - RealmModel realm = session.realms().getRealmByName("test"); - UserLoginFailureModel failure1 = session.sessions().addUserLoginFailure(realm, "user1"); - failure1.incrementFailures(); + testingClient.server().run((KeycloakSession kcSession) -> { + RealmModel realm = kcSession.realms().getRealmByName("test"); + UserLoginFailureModel failure1 = kcSession.loginFailures().addUserLoginFailure(realm, "user1"); + failure1.incrementFailures(); - UserLoginFailureModel failure2 = session.sessions().addUserLoginFailure(realm, "user2"); - failure2.incrementFailures(); - failure2.incrementFailures(); + UserLoginFailureModel failure2 = kcSession.loginFailures().addUserLoginFailure(realm, "user2"); + failure2.incrementFailures(); + failure2.incrementFailures(); + }); - session.getTransactionManager().commit(); + testingClient.server().run((KeycloakSession kcSession) -> { + RealmModel realm = kcSession.realms().getRealmByName("test"); - failure1 = session.sessions().getUserLoginFailure(realm, "user1"); - assertEquals(1, failure1.getNumFailures()); + UserLoginFailureModel failure1 = kcSession.loginFailures().getUserLoginFailure(realm, "user1"); + assertEquals(1, failure1.getNumFailures()); - failure2 = session.sessions().getUserLoginFailure(realm, "user2"); - assertEquals(2, failure2.getNumFailures()); + UserLoginFailureModel failure2 = kcSession.loginFailures().getUserLoginFailure(realm, "user2"); + assertEquals(2, failure2.getNumFailures()); - //session.getTransactionManager().commit(); + // Add the failure, which already exists + failure1.incrementFailures(); - // Add the failure, which already exists - //failure1 = session.sessions().addUserLoginFailure(realm, "user1"); - failure1.incrementFailures(); + assertEquals(2, failure1.getNumFailures()); - //failure1 = session.sessions().getUserLoginFailure(realm, "user1"); - assertEquals(2, failure1.getNumFailures()); + failure1 = kcSession.loginFailures().getUserLoginFailure(realm, "user1"); + failure1.clearFailures(); - failure1 = session.sessions().getUserLoginFailure(realm, "user1"); - failure1.clearFailures(); + failure1 = kcSession.loginFailures().getUserLoginFailure(realm, "user1"); + assertEquals(0, failure1.getNumFailures()); + }); - session.getTransactionManager().commit(); + testingClient.server().run((KeycloakSession kcSession) -> { + RealmModel realm = kcSession.realms().getRealmByName("test"); + kcSession.loginFailures().removeUserLoginFailure(realm, "user1"); + }); - failure1 = session.sessions().getUserLoginFailure(realm, "user1"); - assertEquals(0, failure1.getNumFailures()); + testingClient.server().run((KeycloakSession kcSession) -> { + RealmModel realm = kcSession.realms().getRealmByName("test"); - session.sessions().removeUserLoginFailure(realm, "user1"); - session.sessions().removeUserLoginFailure(realm, "user2"); + assertNull(kcSession.loginFailures().getUserLoginFailure(realm, "user1")); - assertNull(session.sessions().getUserLoginFailure(realm, "user1")); + kcSession.loginFailures().removeAllUserLoginFailures(realm); + }); - session.sessions().removeAllUserLoginFailures(realm); - assertNull(session.sessions().getUserLoginFailure(realm, "user2")); + testingClient.server().run((KeycloakSession kcSession) -> { + RealmModel realm = kcSession.realms().getRealmByName("test"); + assertNull(kcSession.loginFailures().getUserLoginFailure(realm, "user1")); + assertNull(kcSession.loginFailures().getUserLoginFailure(realm, "user2")); + }); } @Test @@ -760,43 +734,20 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { } public static void testOnUserRemoved(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("test"); - UserModel user1 = session.users().getUserByUsername(realm, "user1"); UserModel user2 = session.users().getUserByUsername(realm, "user2"); - UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null); + createSessions(session); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state"); - createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state"); + assertEquals(2, session.sessions().getUserSessionsStream(realm, user1).count()); + assertEquals(1, session.sessions().getUserSessionsStream(realm, user2).count()); - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state"); - - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null); - //createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state"); - AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), sessions[2]); - clientSession.setRedirectUri("http://redirct"); - clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state"); - - - session.sessions().addUserLoginFailure(realm, user1.getId()); - session.sessions().addUserLoginFailure(realm, user2.getId()); - - session.userStorageManager().removeUser(realm, user1); + // remove user1 + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> + (new UserManager(kcSession)).removeUser(realm, user1)); assertEquals(0, session.sessions().getUserSessionsStream(realm, user1).count()); - - session.getTransactionManager().commit(); - - assertNotEquals(0, session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user2")).count()); - - user1 = session.users().getUserByUsername(realm, "user1"); - user2 = session.users().getUserByUsername(realm, "user2"); - - // it seems as if Null does not happen with the new test suite. The sizes of these are ZERO so the removes worked at this point. - //assertNull(session.sessions().getUserLoginFailure(realm, user1.getId())); - //assertNotNull(session.sessions().getUserLoginFailure(realm, user2.getId())); + assertEquals(1, session.sessions().getUserSessionsStream(realm, user2).count()); } private static AuthenticatedClientSessionModel createClientSession(KeycloakSession session, ClientModel client, UserSessionModel userSession, String redirect, String state) { @@ -808,20 +759,21 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { } private static UserSessionModel[] createSessions(KeycloakSession session) { - RealmModel realm = session.realms().getRealmByName("test"); UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null); + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> { + RealmModel realm = kcSession.realms().getRealmByName("test"); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state"); - createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state"); + sessions[0] = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null); - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state"); - - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null); - createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state"); + createClientSession(kcSession, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state"); + createClientSession(kcSession, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state"); + sessions[1] = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); + createClientSession(kcSession, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state"); + sessions[2] = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null); + createClientSession(kcSession, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state"); + }); return sessions; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/BackchannelLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/BackchannelLogoutTest.java index 4c0a92baea..6956af4d1c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/BackchannelLogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/BackchannelLogoutTest.java @@ -529,8 +529,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest { assertNoSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, sessionIdConsumerRealm); - assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, - sessionIdConsumerRealm); + assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm); assertNoSessionsInClient(nbc.subConsumerRealmName(), accountClientIdSubConsumerRealm, userIdSubConsumerRealm, sessionIdSubConsumerRealm); assertActiveSessionInClient(nbc.providerRealmName(), brokerClientIdProviderRealm, userIdProviderRealm, @@ -570,8 +569,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest { assertNoSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, sessionIdConsumerRealm); - assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, - sessionIdConsumerRealm); + assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm); assertNoSessionsInClient(nbc.subConsumerRealmName(), accountClientIdSubConsumerRealm, userIdSubConsumerRealm, sessionIdSubConsumerRealm); assertActiveSessionInClient(nbc.providerRealmName(), brokerClientIdProviderRealm, userIdProviderRealm, @@ -596,8 +594,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest { logoutFromRealm(getConsumerRoot(), nbc.consumerRealmName()); assertNoSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, sessionIdConsumerRealm); - assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, - sessionIdConsumerRealm); + assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm); String logoutTokenEncoded = getLogoutTokenEncodedAndSigned(userIdProviderRealm, sessionIdProviderRealm, true); @@ -606,8 +603,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest { assertThat(response, Matchers.statusCodeIsHC(Response.Status.OK)); } - assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm, - sessionIdConsumerRealm); + assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm); } private void subConsumerIdpRequestsOfflineSessions() { @@ -790,25 +786,23 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest { .collect(Collectors.toList()); } - private void assertActiveOfflineSessionInClient(String realmName, String clientId, String userId, - String sessionId) { - List sessions = getOfflineClientSessions(realmName, clientId, userId, sessionId); + private void assertActiveOfflineSessionInClient(String realmName, String clientId, String userId) { + List sessions = getOfflineClientSessions(realmName, clientId, userId); assertThat(sessions.size(), is(1)); } - private void assertNoOfflineSessionsInClient(String realmName, String clientId, String userId, String sessionId) { - List sessions = getOfflineClientSessions(realmName, clientId, userId, sessionId); + private void assertNoOfflineSessionsInClient(String realmName, String clientId, String userId) { + List sessions = getOfflineClientSessions(realmName, clientId, userId); assertThat(sessions.size(), is(0)); } - private List getOfflineClientSessions(String realmName, String clientUuid, String userId, - String sessionId) { + private List getOfflineClientSessions(String realmName, String clientUuid, String userId) { return adminClient.realm(realmName) .clients() .get(clientUuid) .getOfflineUserSessions(0, 5) .stream() - .filter(s -> s.getUserId().equals(userId) && s.getId().equals(sessionId)) + .filter(s -> s.getUserId().equals(userId)) .collect(Collectors.toList()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index 0c998d2b7f..9404002535 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -38,6 +38,7 @@ import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AdminRoles; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.protocol.oidc.OIDCConfigAttributes; @@ -255,17 +256,17 @@ public class OfflineTokenTest extends AbstractKeycloakTest { setTimeOffset(3000000); OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(newRefreshTokenString, "secret1"); + RefreshToken newRefreshToken = oauth.parseRefreshToken(newRefreshTokenString); Assert.assertEquals(400, response.getStatusCode()); assertEquals("invalid_grant", response.getError()); - events.expectRefresh(offlineToken.getId(), sessionId) + events.expectRefresh(offlineToken.getId(), newRefreshToken.getSessionState()) .client("offline-client") .error(Errors.INVALID_TOKEN) .user(userId) .clearDetails() .assertEvent(); - setTimeOffset(0); } @@ -287,7 +288,6 @@ public class OfflineTokenTest extends AbstractKeycloakTest { OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1"); AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken()); Assert.assertEquals(200, response.getStatusCode()); - Assert.assertEquals(sessionId, refreshedToken.getSessionState()); // Assert new refreshToken in the response String newRefreshToken = response.getRefreshToken(); @@ -393,7 +393,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest { .assertEvent(); // Refresh with new refreshToken is successful now - testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, token.getSessionState(), userId); + testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, offlineToken2.getSessionState(), userId); RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false); } @@ -590,6 +590,40 @@ public class OfflineTokenTest extends AbstractKeycloakTest { response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1"); assertEquals(400, response.getStatusCode()); } + + @Test + public void onlineOfflineTokenLogout() throws Exception { + oauth.clientId("offline-client"); + + // create online session + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password"); + assertEquals(200, response.getStatusCode()); + + // assert refresh token + response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1"); + assertEquals(200, response.getStatusCode()); + + // create offline session + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + OAuthClient.AccessTokenResponse offlineResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password"); + assertEquals(200, offlineResponse.getStatusCode()); + + // assert refresh offline token + OAuthClient.AccessTokenResponse offlineRefresh = oauth.doRefreshTokenRequest(offlineResponse.getRefreshToken(), "secret1"); + assertEquals(200, offlineRefresh.getStatusCode()); + + // logout online session + CloseableHttpResponse logoutResponse = oauth.scope("").doLogout(response.getRefreshToken(), "secret1"); + assertEquals(204, logoutResponse.getStatusLine().getStatusCode()); + + // assert the online session is gone + response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1"); + assertEquals(400, response.getStatusCode()); + + // assert the offline token refresh still works + offlineRefresh = oauth.doRefreshTokenRequest(offlineResponse.getRefreshToken(), "secret1"); + assertEquals(200, offlineRefresh.getStatusCode()); + } @Test public void browserOfflineTokenLogoutFollowedByLoginSameSession() throws Exception { @@ -621,11 +655,14 @@ public class OfflineTokenTest extends AbstractKeycloakTest { assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); assertEquals(0, offlineToken.getExpiration()); + String offlineUserSessionId = testingClient.server().fetch((KeycloakSession session) -> + session.sessions().getOfflineUserSession(session.realms().getRealmByName("test"), offlineToken.getSessionState()).getId(), String.class); + + // logout offline session try (CloseableHttpResponse logoutResponse = oauth.doLogout(offlineTokenString, "secret1")) { assertEquals(204, logoutResponse.getStatusLine().getStatusCode()); } - - events.expectLogout(offlineToken.getSessionState()) + events.expectLogout(offlineUserSessionId) .client("offline-client") .removeDetail(Details.REDIRECT_URI) .assertEvent(); @@ -752,7 +789,6 @@ public class OfflineTokenTest extends AbstractKeycloakTest { offlineToken = oauth.parseRefreshToken(offlineTokenString); Assert.assertEquals(200, tokenResponse.getStatusCode()); - Assert.assertEquals(sessionId, refreshedToken.getSessionState()); // wait to expire setTimeOffset(offset); 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 cf1a377674..196c8d6c4c 100755 --- 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 @@ -64,6 +64,14 @@ "provider": "${keycloak.authSession.provider:infinispan}" }, + "userSessions": { + "provider": "${keycloak.userSession.provider:infinispan}" + }, + + "loginFailure": { + "provider": "${keycloak.loginFailure.provider:infinispan}" + }, + "mapStorage": { "provider": "${keycloak.mapStorage.provider:concurrenthashmap}", "concurrenthashmap": { @@ -93,10 +101,6 @@ } }, - "userSessions": { - "provider" : "${keycloak.userSessions.provider:infinispan}" - }, - "timer": { "provider": "basic" }, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json index 7155790579..d9b92b492b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json @@ -3,7 +3,10 @@ "realm": "test", "enabled": true, "accessTokenLifespan": 10, - "ssoSessionIdleTimeout": 30, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespan": 5184000, "sslRequired": "external", "registrationAllowed": true, "resetPasswordAllowed": true, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json index 82f3bda663..c96d9b8e89 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json @@ -6,6 +6,10 @@ "registrationAllowed": true, "resetPasswordAllowed": true, "editUsernameAllowed" : true, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespan": 5184000, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], diff --git a/testsuite/model/src/main/java/org/keycloak/testsuite/model/ResteasyNullProvider.java b/testsuite/model/src/main/java/org/keycloak/testsuite/model/ResteasyNullProvider.java new file mode 100644 index 0000000000..86d15f22a7 --- /dev/null +++ b/testsuite/model/src/main/java/org/keycloak/testsuite/model/ResteasyNullProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.model; + +import org.keycloak.common.util.ResteasyProvider; + +/** + * @author Martin Kanis + */ +public class ResteasyNullProvider implements ResteasyProvider { + + @Override + public R getContextData(Class type) { + return null; + } + + @Override + public void pushDefaultContextObject(Class type, Object instance) { + + } + + @Override + public void pushContext(Class type, Object instance) { + + } + + @Override + public void clearContextData() { + + } +} diff --git a/testsuite/model/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider b/testsuite/model/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider new file mode 100644 index 0000000000..5c7f0b9fbe --- /dev/null +++ b/testsuite/model/src/main/resources/META-INF/services/org.keycloak.common.util.ResteasyProvider @@ -0,0 +1,17 @@ +# +# Copyright 2021 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +org.keycloak.testsuite.model.ResteasyNullProvider \ No newline at end of file diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java index 1d16c312e8..9ba9b3a88b 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java @@ -33,8 +33,11 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmSpi; import org.keycloak.models.RoleSpi; +import org.keycloak.models.UserLoginFailureSpi; +import org.keycloak.models.UserSessionSpi; import org.keycloak.models.UserSpi; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderManager; @@ -67,6 +70,7 @@ import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.keycloak.timer.TimerSpi; /** * Base of testcases that operate on session level. The tests derived from this class @@ -172,6 +176,9 @@ public abstract class KeycloakModelTest { .add(RealmSpi.class) .add(RoleSpi.class) .add(StoreFactorySpi.class) + .add(TimerSpi.class) + .add(UserLoginFailureSpi.class) + .add(UserSessionSpi.class) .add(UserSpi.class) .build(); @@ -228,15 +235,15 @@ public abstract class KeycloakModelTest { } }; res.init(); + res.publish(new PostMigrationEvent()); return res; } public static void reinitializeKeycloakSessionFactory() { - DefaultKeycloakSessionFactory f = createKeycloakSessionFactory(); if (FACTORY != null) { FACTORY.close(); } - FACTORY = f; + FACTORY = createKeycloakSessionFactory(); } @BeforeClass diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java new file mode 100644 index 0000000000..04eda55caa --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.model; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.UserManager; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider; +import org.keycloak.services.managers.UserSessionManager; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author Marek Posolda + * @author Martin Bartos + * @author Martin Kanis + */ +@RequireProvider(UserSessionPersisterProvider.class) +@RequireProvider(UserSessionProvider.class) +@RequireProvider(UserProvider.class) +@RequireProvider(RealmProvider.class) +public class UserSessionInitializerTest extends KeycloakModelTest { + + private String realmId; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("test"); + realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + realm.setSsoSessionIdleTimeout(1800); + realm.setSsoSessionMaxLifespan(36000); + this.realmId = realm.getId(); + + s.users().addUser(realm, "user1").setEmail("user1@localhost"); + s.users().addUser(realm, "user2").setEmail("user2@localhost"); + + UserSessionPersisterProviderTest.createClients(s, realm); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().getRealm(realmId); + s.sessions().removeUserSessions(realm); + + UserModel user1 = s.users().getUserByUsername(realm, "user1"); + UserModel user2 = s.users().getUserByUsername(realm, "user2"); + + UserManager um = new UserManager(s); + if (user1 != null) { + um.removeUser(realm, user1); + } + if (user2 != null) { + um.removeUser(realm, user2); + } + + s.realms().removeRealm(realmId); + } + + @Test + public void testUserSessionInitializer() { + String[] origSessionIds = createSessionsInPersisterOnly(); + int started = Time.currentTime(); + + reinitializeKeycloakSessionFactory(); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // Assert sessions are in + ClientModel testApp = realm.getClientByClientId("test-app"); + ClientModel thirdparty = realm.getClientByClientId("third-party"); + + assertThat("Count of offline sesions for client 'test-app'", session.sessions().getOfflineSessionsCount(realm, testApp), is((long) 3)); + assertThat("Count of offline sesions for client 'third-party'", session.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1)); + + List loadedSessions = session.sessions().getOfflineUserSessionsStream(realm, testApp, 0, 10) + .collect(Collectors.toList()); + UserSessionPersisterProviderTest.assertSessions(loadedSessions, origSessionIds); + + assertSessionLoaded(loadedSessions, origSessionIds[0], session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party"); + assertSessionLoaded(loadedSessions, origSessionIds[1], session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); + assertSessionLoaded(loadedSessions, origSessionIds[2], session.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app"); + }); + } + + @Test + public void testUserSessionInitializerWithDeletingClient() { + String[] origSessionIds = createSessionsInPersisterOnly(); + int started = Time.currentTime(); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // Delete one of the clients now + ClientModel testApp = realm.getClientByClientId("test-app"); + realm.removeClient(testApp.getId()); + }); + + reinitializeKeycloakSessionFactory(); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // Assert sessions are in + ClientModel thirdparty = realm.getClientByClientId("third-party"); + + assertThat("Count of offline sesions for client 'third-party'", session.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1)); + List loadedSessions = session.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10) + .collect(Collectors.toList()); + + assertThat("Size of loaded Sessions", loadedSessions.size(), is(1)); + assertSessionLoaded(loadedSessions, origSessionIds[0], session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "third-party"); + + // Revert client + realm.addClient("test-app"); + }); + + } + + // Create sessions in persister + infinispan, but then delete them from infinispan cache. This is to allow later testing of initializer. Return the list of "origSessions" + private String[] createSessionsInPersisterOnly() { + UserSessionModel[] origSessions = inComittedTransaction(session -> { return UserSessionPersisterProviderTest.createSessions(session, realmId); }); + String[] res = new String[origSessions.length]; + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + UserSessionManager sessionManager = new UserSessionManager(session); + + int i = 0; + for (UserSessionModel origSession : origSessions) { + UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId()); + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { + sessionManager.createOrUpdateOfflineSession(clientSession, userSession); + } + String cs = userSession.getNote(UserSessionModel.CORRESPONDING_SESSION_ID); + res[i] = cs == null ? userSession.getId() : cs; + i++; + } + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // Delete local user cache (persisted sessions are still kept) + UserSessionProvider provider = session.getProvider(UserSessionProvider.class); + if (provider instanceof InfinispanUserSessionProvider) { + // Remove in-memory representation of the offline sessions + ((InfinispanUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true); + + // Clear ispn cache to ensure initializerState is removed as well + InfinispanConnectionProvider infinispan = session.getProvider(InfinispanConnectionProvider.class); + if (infinispan != null) { + infinispan.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME).clear(); + } + } + }); + + inComittedTransaction(session -> { + // This is only valid in infinispan provider where the offline session is loaded upon start and never reloaded + UserSessionProvider provider = session.getProvider(UserSessionProvider.class); + if (provider instanceof InfinispanUserSessionProvider) { + RealmModel realm = session.realms().getRealm(realmId); + + ClientModel testApp = realm.getClientByClientId("test-app"); + ClientModel thirdparty = realm.getClientByClientId("third-party"); + assertThat("Count of offline sessions for client 'test-app'", session.sessions().getOfflineSessionsCount(realm, testApp), is((long) 0)); + assertThat("Count of offline sessions for client 'third-party'", session.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 0)); + } + }); + + return res; + } + + private 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)) { + UserSessionPersisterProviderTest.assertSession(session, user, ipAddress, started, lastRefresh, clients); + return; + } + } + Assert.fail("Session with ID " + id + " not found in the list"); + } +} + diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java new file mode 100644 index 0000000000..73841cd72d --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -0,0 +1,625 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.model; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.UserManager; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.utils.ResetTimeOffsetEvent; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; +import org.keycloak.models.Constants; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; +import org.hamcrest.Matchers; + +/** + * @author Marek Posolda + * @author Martin Kanis + */ +@RequireProvider(UserSessionPersisterProvider.class) +@RequireProvider(value = UserSessionProvider.class, only = InfinispanUserSessionProviderFactory.PROVIDER_ID) +@RequireProvider(UserProvider.class) +@RequireProvider(RealmProvider.class) +public class UserSessionPersisterProviderTest extends KeycloakModelTest { + + private String realmId; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("test"); + realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + this.realmId = realm.getId(); + + s.users().addUser(realm, "user1").setEmail("user1@localhost"); + s.users().addUser(realm, "user2").setEmail("user2@localhost"); + + createClients(s, realm); + } + + protected static void createClients(KeycloakSession s, RealmModel realm) { + ClientModel clientModel = s.clients().addClient(realm, "test-app"); + clientModel.setEnabled(true); + clientModel.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth"); + Set redirects = new HashSet<>(Arrays.asList("http://localhost:8180/auth/realms/master/app/auth/*", + "https://localhost:8543/auth/realms/master/app/auth/*", + "http://localhost:8180/auth/realms/test/app/auth/*", + "https://localhost:8543/auth/realms/test/app/auth/*")); + clientModel.setRedirectUris(redirects); + clientModel.setSecret("password"); + + clientModel = s.clients().addClient(realm, "third-party"); + clientModel.setEnabled(true); + clientModel.setConsentRequired(true); + clientModel.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth"); + clientModel.setRedirectUris(redirects); + clientModel.setSecret("password"); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().getRealm(realmId); + s.sessions().removeUserSessions(realm); + + UserModel user1 = s.users().getUserByUsername(realm, "user1"); + UserModel user2 = s.users().getUserByUsername(realm, "user2"); + + UserManager um = new UserManager(s); + if (user1 != null) { + um.removeUser(realm, user1); + } + if (user2 != null) { + um.removeUser(realm, user2); + } + + s.realms().removeRealm(realmId); + } + + @Test + public void testPersistenceWithLoad() { + int started = Time.currentTime(); + final UserSessionModel[] userSession = new UserSessionModel[1]; + + UserSessionModel[] origSessions = inComittedTransaction(session -> { + // Create some sessions in infinispan + return createSessions(session, realmId); + }); + + inComittedTransaction(session -> { + // Persist 3 created userSessions and clientSessions as offline + RealmModel realm = session.realms().getRealm(realmId); + ClientModel testApp = realm.getClientByClientId("test-app"); + session.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList()) + .forEach(userSessionLooper -> persistUserSession(session, userSessionLooper, true)); + }); + + inComittedTransaction(session -> { + // Persist 1 online session + RealmModel realm = session.realms().getRealm(realmId); + userSession[0] = session.sessions().getUserSession(realm, origSessions[0].getId()); + persistUserSession(session, userSession[0], false); + }); + + inComittedTransaction(session -> { // Assert online session + RealmModel realm = session.realms().getRealm(realmId); + List loadedSessions = loadPersistedSessionsPaginated(session, false, 1, 1, 1); + assertSession(loadedSessions.get(0), session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party"); + }); + + inComittedTransaction(session -> { + // Assert offline sessions + RealmModel realm = session.realms().getRealm(realmId); + List loadedSessions = loadPersistedSessionsPaginated(session, true, 2, 2, 3); + assertSessions(loadedSessions, new String[] { origSessions[0].getId(), origSessions[1].getId(), origSessions[2].getId() }); + + assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party"); + assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); + assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app"); + }); + } + + @Test + public void testUpdateAndRemove() { + int started = Time.currentTime(); + + AtomicReference origSessionsAt = new AtomicReference<>(); + AtomicReference> loadedSessionsAt = new AtomicReference<>(); + + AtomicReference userSessionAt = new AtomicReference<>(); + AtomicReference persistedSessionAt = new AtomicReference<>(); + + inComittedTransaction(session -> { + // Create some sessions in infinispan + UserSessionModel[] origSessions = createSessions(session, realmId); + origSessionsAt.set(origSessions); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + UserSessionModel[] origSessions = origSessionsAt.get(); + + // Persist 1 offline session + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[1].getId()); + userSessionAt.set(userSession); + + persistUserSession(session, userSession, true); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + + // Load offline session + List loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1); + loadedSessionsAt.set(loadedSessions); + + UserSessionModel persistedSession = loadedSessions.get(0); + persistedSessionAt.set(persistedSession); + + assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); + + // create new clientSession + AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()), + "http://redirect", "state"); + persister.createClientSession(clientSession, true); + + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + UserSessionModel userSession = userSessionAt.get(); + + // Remove clientSession + persister.removeClientSession(userSession.getId(), realm.getClientByClientId("third-party").getId(), true); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + + // Assert clientSession removed + List loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1); + UserSessionModel persistedSession = loadedSessions.get(0); + assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); + + // Remove userSession + persister.removeUserSession(persistedSession.getId(), true); + }); + + inComittedTransaction(session -> { + // Assert nothing found + loadPersistedSessionsPaginated(session, true, 10, 0, 0); + }); + } + + @Test + public void testOnRealmRemoved() { + AtomicReference userSessionID = new AtomicReference<>(); + + inComittedTransaction(session -> { + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.setDefaultRole(session.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName())); + + fooRealm.addClient("foo-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null); + userSessionID.set(userSession.getId()); + + createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); + }); + + inComittedTransaction(session -> { + // Persist offline session + RealmModel fooRealm = session.realms().getRealm("foo"); + UserSessionModel userSession = session.sessions().getUserSession(fooRealm, userSessionID.get()); + persistUserSession(session, userSession, true); + }); + + inComittedTransaction(session -> { + // Assert session was persisted + loadPersistedSessionsPaginated(session, true, 10, 1, 1); + + // Remove realm + RealmManager realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + }); + + inComittedTransaction(session -> { + // Assert nothing loaded + loadPersistedSessionsPaginated(session, true, 10, 0, 0); + }); + } + + @Test + public void testOnClientRemoved() { + int started = Time.currentTime(); + AtomicReference userSessionID = new AtomicReference<>(); + + inComittedTransaction(session -> { + RealmModel fooRealm = session.realms().createRealm("foo", "foo"); + fooRealm.setDefaultRole(session.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX)); + + fooRealm.addClient("foo-app"); + fooRealm.addClient("bar-app"); + session.users().addUser(fooRealm, "user3"); + + UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null); + userSessionID.set(userSession.getId()); + + createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); + createClientSession(session, realmId, fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state"); + }); + + inComittedTransaction(session -> { + RealmModel fooRealm = session.realms().getRealm("foo"); + + // Persist offline session + UserSessionModel userSession = session.sessions().getUserSession(fooRealm, userSessionID.get()); + persistUserSession(session, userSession, true); + }); + + inComittedTransaction(session -> { + RealmManager realmMgr = new RealmManager(session); + ClientManager clientMgr = new ClientManager(realmMgr); + RealmModel fooRealm = realmMgr.getRealm("foo"); + + // Assert session was persisted with both clientSessions + UserSessionModel persistedSession = loadPersistedSessionsPaginated(session, true, 10, 1, 1).get(0); + assertSession(persistedSession, session.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "foo-app", "bar-app"); + + // Remove foo-app client + ClientModel client = fooRealm.getClientByClientId("foo-app"); + clientMgr.removeClient(fooRealm, client); + }); + + inComittedTransaction(session -> { + RealmManager realmMgr = new RealmManager(session); + ClientManager clientMgr = new ClientManager(realmMgr); + RealmModel fooRealm = realmMgr.getRealm("foo"); + + // Assert just one bar-app clientSession persisted now + UserSessionModel persistedSession = loadPersistedSessionsPaginated(session, true, 10, 1, 1).get(0); + assertSession(persistedSession, session.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "bar-app"); + + // Remove bar-app client + ClientModel client = fooRealm.getClientByClientId("bar-app"); + clientMgr.removeClient(fooRealm, client); + }); + + inComittedTransaction(session -> { + // Assert loading still works - last userSession is still there, but no clientSession on it + loadPersistedSessionsPaginated(session, true, 10, 1, 1); + + // Cleanup + RealmManager realmMgr = new RealmManager(session); + realmMgr.removeRealm(realmMgr.getRealm("foo")); + }); + } + + @Test + public void testOnUserRemoved() { + int started = Time.currentTime(); + AtomicReference origSessionsAt = new AtomicReference<>(); + + inComittedTransaction(session -> { + // Create some sessions in infinispan + UserSessionModel[] origSessions = createSessions(session, realmId); + origSessionsAt.set(origSessions); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionModel[] origSessions = origSessionsAt.get(); + + // Persist 2 offline sessions of 2 users + UserSessionModel userSession1 = session.sessions().getUserSession(realm, origSessions[1].getId()); + UserSessionModel userSession2 = session.sessions().getUserSession(realm, origSessions[2].getId()); + persistUserSession(session, userSession1, true); + persistUserSession(session, userSession2, true); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // Load offline sessions + loadPersistedSessionsPaginated(session, true, 10, 1, 2); + + // Properly delete user and assert his offlineSession removed + UserModel user1 = session.users().getUserByUsername(realm, "user1"); + new UserManager(session).removeUser(realm, user1); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + Assert.assertEquals(1, persister.getUserSessionsCount(true)); + + List loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1); + UserSessionModel persistedSession = loadedSessions.get(0); + assertSession(persistedSession, session.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app"); + + // KEYCLOAK-2431 Assert that userSessionPersister is resistent even to situation, when users are deleted "directly". + // No exception will happen. However session will be still there + UserModel user2 = session.users().getUserByUsername(realm, "user2"); + session.users().removeUser(realm, user2); + + loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1); + + // Cleanup + UserSessionModel userSession = loadedSessions.get(0); + session.sessions().removeUserSession(realm, userSession); + persister.removeUserSession(userSession.getId(), userSession.isOffline()); + }); + } + + // KEYCLOAK-1999 + @Test + public void testNoSessions() { + inComittedTransaction(session -> { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + Stream sessions = persister.loadUserSessionsStream(0, 1, true, 0, "abc"); + Assert.assertEquals(0, sessions.count()); + }); + } + + @Test + public void testMoreSessions() { + AtomicReference> userSessionsAt = new AtomicReference<>(); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // Create 10 userSessions - each having 1 clientSession + List userSessions = new ArrayList<>(); + UserModel user = session.users().getUserByUsername(realm, "user1"); + + for (int i = 0; i < 20; i++) { + // Having different offsets for each session (to ensure that lastSessionRefresh is also different) + Time.setOffset(i); + + UserSessionModel userSession = session.sessions().createUserSession(realm, user, "user1", "127.0.0.1", "form", true, null, null); + createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state"); + userSessions.add(userSession); + } + + userSessionsAt.set(userSessions); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + List userSessions = userSessionsAt.get(); + + for (UserSessionModel userSession : userSessions) { + UserSessionModel userSession2 = session.sessions().getUserSession(realm, userSession.getId()); + persistUserSession(session, userSession2, true); + } + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + List loadedSessions = loadPersistedSessionsPaginated(session, true, 2, 10, 20); + UserModel user = session.users().getUserByUsername(realm, "user1"); + ClientModel testApp = realm.getClientByClientId("test-app"); + + for (UserSessionModel loadedSession : loadedSessions) { + assertEquals(user.getId(), loadedSession.getUser().getId()); + assertEquals("127.0.0.1", loadedSession.getIpAddress()); + assertEquals(user.getUsername(), loadedSession.getLoginUsername()); + + assertEquals(1, loadedSession.getAuthenticatedClientSessions().size()); + assertTrue(loadedSession.getAuthenticatedClientSessions().containsKey(testApp.getId())); + } + }); + } + + @Test + public void testExpiredSessions() { + int started = Time.currentTime(); + final UserSessionModel[] userSession1 = {null}; + final UserSessionModel[] userSession2 = {null}; + + UserSessionModel[] origSessions = inComittedTransaction(session -> { + // Create some sessions in infinispan + return createSessions(session, realmId); + }); + + inComittedTransaction(session -> { + // Persist 2 offline sessions of 2 users + RealmModel realm = session.realms().getRealm(realmId); + userSession1[0] = session.sessions().getUserSession(realm, origSessions[1].getId()); + userSession2[0] = session.sessions().getUserSession(realm, origSessions[2].getId()); + persistUserSession(session, userSession1[0], true); + persistUserSession(session, userSession2[0], true); + }); + + inComittedTransaction(session -> { + // Update one of the sessions with lastSessionRefresh of 20 days ahead + int lastSessionRefresh = Time.currentTime() + 1728000; + RealmModel realm = session.realms().getRealm(realmId); + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + + persister.updateLastSessionRefreshes(realm, lastSessionRefresh, Collections.singleton(userSession1[0].getId()), true); + + // Increase time offset - 40 days + Time.setOffset(3456000); + try { + // Run expiration thread + persister.removeExpired(realm); + + // Test the updated session is still in persister. Not updated session is not there anymore + List loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1); + UserSessionModel persistedSession = loadedSessions.get(0); + assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, lastSessionRefresh, "test-app"); + + } finally { + // Cleanup + Time.setOffset(0); + session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent()); + } + }); + } + + protected static AuthenticatedClientSessionModel createClientSession(KeycloakSession session, String realmId, ClientModel client, UserSessionModel userSession, String redirect, String state) { + RealmModel realm = session.realms().getRealm(realmId); + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); + clientSession.setRedirectUri(redirect); + if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); + return clientSession; + } + + protected static UserSessionModel[] createSessions(KeycloakSession session, String realmId) { + RealmModel realm = session.realms().getRealm(realmId); + UserSessionModel[] sessions = new UserSessionModel[3]; + sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null); + + createClientSession(session, realmId, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state"); + createClientSession(session, realmId, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state"); + + sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null); + createClientSession(session, realmId, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state"); + + sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null); + createClientSession(session, realmId, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state"); + + return sessions; + } + + private void persistUserSession(KeycloakSession session, UserSessionModel userSession, boolean offline) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + persister.createUserSession(userSession, offline); + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { + persister.createClientSession(clientSession, offline); + } + } + + 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)) { + assertSession(session, user, ipAddress, started, lastRefresh, clients); + return; + } + } + Assert.fail("Session with ID " + id + " not found in the list"); + } + + private List loadPersistedSessionsPaginated(KeycloakSession session, boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + + int count = persister.getUserSessionsCount(offline); + + int pageCount = 0; + boolean next = true; + List result = new ArrayList<>(); + int lastCreatedOn = 0; + String lastSessionId = "abc"; + + while (next) { + List sess = persister + .loadUserSessionsStream(0, sessionsPerPage, offline, lastCreatedOn, lastSessionId) + .collect(Collectors.toList()); + + if (sess.size() < sessionsPerPage) { + next = false; + + // We had at least some session + if (sess.size() > 0) { + pageCount++; + } + } else { + pageCount++; + + UserSessionModel lastSession = sess.get(sess.size() - 1); + lastCreatedOn = lastSession.getStarted(); + lastSessionId = lastSession.getId(); + } + + result.addAll(sess); + } + + Assert.assertEquals(expectedPageCount, pageCount); + Assert.assertEquals(expectedSessionsCount, result.size()); + return result; + } + + 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()); + assertEquals("form", session.getAuthMethod()); + assertTrue(session.isRememberMe()); + assertTrue(session.getStarted() >= started - 1 && session.getStarted() <= started + 1); + assertTrue(session.getLastSessionRefresh() >= lastRefresh - 1 && session.getLastSessionRefresh() <= lastRefresh + 1); + + String[] actualClients = new String[session.getAuthenticatedClientSessions().size()]; + int i = 0; + for (Map.Entry entry : session.getAuthenticatedClientSessions().entrySet()) { + String clientUUID = entry.getKey(); + AuthenticatedClientSessionModel clientSession = entry.getValue(); + Assert.assertEquals(clientUUID, clientSession.getClient().getId()); + actualClients[i] = clientSession.getClient().getClientId(); + i++; + } + + assertThat(actualClients, Matchers.arrayContainingInAnyOrder(clients)); + } + + public static void assertSessions(List actualSessions, String[] expectedSessionIds) { + String[] actual = new String[actualSessions.size()]; + for (int i = 0; i < actual.length; i++) { + actual[i] = actualSessions.get(i).getId(); + } + + assertThat(actual, Matchers.arrayContainingInAnyOrder(expectedSessionIds)); + } +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderModelTest.java new file mode 100644 index 0000000000..f6443aa82c --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderModelTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.model; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.UserManager; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory; +import org.keycloak.models.utils.ResetTimeOffsetEvent; +import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil; +import org.keycloak.timer.TimerProvider; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import static org.keycloak.testsuite.model.UserSessionPersisterProviderTest.createClients; +import static org.keycloak.testsuite.model.UserSessionPersisterProviderTest.createSessions; + +/** + * @author Martin Kanis + */ +@RequireProvider(UserSessionPersisterProvider.class) +@RequireProvider(UserSessionProvider.class) +@RequireProvider(UserProvider.class) +@RequireProvider(RealmProvider.class) +public class UserSessionProviderModelTest extends KeycloakModelTest { + + private String realmId; + private KeycloakSession kcSession; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("test"); + realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + realm.setSsoSessionIdleTimeout(1800); + realm.setSsoSessionMaxLifespan(36000); + this.realmId = realm.getId(); + this.kcSession = s; + + s.users().addUser(realm, "user1").setEmail("user1@localhost"); + s.users().addUser(realm, "user2").setEmail("user2@localhost"); + + createClients(s, realm); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().getRealm(realmId); + s.sessions().removeUserSessions(realm); + + UserModel user1 = s.users().getUserByUsername(realm, "user1"); + UserModel user2 = s.users().getUserByUsername(realm, "user2"); + + UserManager um = new UserManager(s); + if (user1 != null) { + um.removeUser(realm, user1); + } + if (user2 != null) { + um.removeUser(realm, user2); + } + + s.realms().removeRealm(realmId); + } + + @Test + public void testMultipleSessionsRemovalInOneTransaction() { + UserSessionModel[] origSessions = inComittedTransaction(session -> { return createSessions(session, realmId); }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + Assert.assertEquals(origSessions[0], userSession); + + userSession = session.sessions().getUserSession(realm, origSessions[1].getId()); + Assert.assertEquals(origSessions[1], userSession); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + session.sessions().removeUserSession(realm, origSessions[0]); + session.sessions().removeUserSession(realm, origSessions[1]); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + Assert.assertNull(userSession); + + userSession = session.sessions().getUserSession(realm, origSessions[1].getId()); + Assert.assertNull(userSession); + }); + } + + @Test + public void testExpiredClientSessions() { + // Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider + TimerProvider timer = kcSession.getProvider(TimerProvider.class); + TimerProvider.TimerTaskContext timerTaskCtx = null; + if (timer != null) { + timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); + log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); + + InfinispanTestUtil.setTestingTimeService(kcSession); + } + + AtomicReference> clientSessionIds = new AtomicReference<>(); + + try { + UserSessionModel[] origSessions = inComittedTransaction(session -> { + // create some user and client sessions + return createSessions(session, realmId); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + Assert.assertEquals(origSessions[0], userSession); + + AuthenticatedClientSessionModel clientSession = session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"), + UUID.fromString(origSessions[0].getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()).getId()), + false); + Assert.assertEquals(origSessions[0].getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()).getId(), clientSession.getId()); + + userSession = session.sessions().getUserSession(realm, origSessions[1].getId()); + Assert.assertEquals(origSessions[1], userSession); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + + Collection values = userSession.getAuthenticatedClientSessions().values(); + List clientSessions = new LinkedList<>(); + values.stream().forEach(clientSession -> { + // expire client sessions + clientSession.setTimestamp(1); + clientSessions.add(clientSession.getId()); + }); + clientSessionIds.set(clientSessions); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + + // assert the user session is still there + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + Assert.assertEquals(origSessions[0], userSession); + + // assert the client sessions are expired + clientSessionIds.get().forEach(clientSessionId -> + Assert.assertNull(session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"), UUID.fromString(clientSessionId), false))); + }); + } finally { + Time.setOffset(0); + kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent()); + if (timer != null && timerTaskCtx != null) { + timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); + + InfinispanTestUtil.revertTimeService(kcSession); + } + } + } +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineModelTest.java new file mode 100644 index 0000000000..23f86c4dd9 --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineModelTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.model; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.UserManager; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory; +import org.keycloak.models.utils.ResetTimeOffsetEvent; +import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil; +import org.keycloak.timer.TimerProvider; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Marek Posolda + * @author Martin Kanis + */ +@RequireProvider(UserSessionPersisterProvider.class) +@RequireProvider(value=UserSessionProvider.class, only={"infinispan"}) +@RequireProvider(UserProvider.class) +@RequireProvider(RealmProvider.class) +public class UserSessionProviderOfflineModelTest extends KeycloakModelTest { + + private String realmId; + private KeycloakSession kcSession; + + private UserSessionManager sessionManager; + private UserSessionPersisterProvider persister; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("test"); + realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + this.realmId = realm.getId(); + this.kcSession = s; + + s.users().addUser(realm, "user1").setEmail("user1@localhost"); + s.users().addUser(realm, "user2").setEmail("user2@localhost"); + + UserSessionPersisterProviderTest.createClients(s, realm); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().getRealm(realmId); + s.sessions().removeUserSessions(realm); + + UserModel user1 = s.users().getUserByUsername(realm, "user1"); + UserModel user2 = s.users().getUserByUsername(realm, "user2"); + + UserManager um = new UserManager(s); + if (user1 != null) { + um.removeUser(realm, user1); + } + if (user2 != null) { + um.removeUser(realm, user2); + } + + s.realms().removeRealm(realmId); + } + + @Test + public void testExpired() { + // Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider + TimerProvider timer = kcSession.getProvider(TimerProvider.class); + TimerProvider.TimerTaskContext timerTaskCtx = null; + if (timer != null) { + timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); + log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); + } + + InfinispanTestUtil.setTestingTimeService(kcSession); + + try { + // Key is userSessionId, value is set of client UUIDS + Map> offlineSessions = new HashMap<>(); + ClientModel[] testApp = new ClientModel[1]; + + UserSessionModel[] origSessions = inComittedTransaction(session -> { + // Create some online sessions in infinispan + return UserSessionPersisterProviderTest.createSessions(session, realmId); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + sessionManager = new UserSessionManager(session); + persister = session.getProvider(UserSessionPersisterProvider.class); + + // Persist 3 created userSessions and clientSessions as offline + testApp[0] = realm.getClientByClientId("test-app"); + session.sessions().getUserSessionsStream(realm, testApp[0]).collect(Collectors.toList()) + .forEach(userSession -> offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(session, userSession))); + + // Assert all previously saved offline sessions found + for (Map.Entry> entry : offlineSessions.entrySet()) { + UserSessionModel foundSession = sessionManager.findOfflineUserSession(realm, entry.getKey()); + Assert.assertEquals(foundSession.getAuthenticatedClientSessions().keySet(), entry.getValue()); + } + }); + + log.info("Persisted 3 sessions to UserSessionPersisterProvider"); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + persister = session.getProvider(UserSessionPersisterProvider.class); + + UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId()); + Assert.assertNotNull(session0); + + // sessions are in persister too + Assert.assertEquals(3, persister.getUserSessionsCount(true)); + + Time.setOffset(300); + log.infof("Set time offset to 300. Time is: %d", Time.currentTime()); + + // Set lastSessionRefresh to currentSession[0] to 0 + session0.setLastSessionRefresh(Time.currentTime()); + }); + + + // Increase timeOffset and update LSR of the session two times - first to 20 days and then to 21 days. At least one of updates + // will propagate to PersisterLastSessionRefreshStore and update DB (Single update is not 100% sure as there is still a + // chance of delayed periodic task to be run in the meantime and causing race-condition, which would mean LSR not updated in the DB) + IntStream.range(0, 2).sequential().forEach(index -> inComittedTransaction(index, (session, i) -> { + int timeOffset = 1728000 + (i * 86400); + + RealmModel realm = session.realms().getRealm(realmId); + Time.setOffset(timeOffset); + log.infof("Set time offset to %d. Time is: %d", timeOffset, Time.currentTime()); + + UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId()); + session0.setLastSessionRefresh(Time.currentTime()); + + return null; + })); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + persister = session.getProvider(UserSessionPersisterProvider.class); + + // Increase timeOffset - 40 days + Time.setOffset(3456000); + log.infof("Set time offset to 3456000. Time is: %d", Time.currentTime()); + + // Expire and ensure that all sessions despite session0 were removed + persister.removeExpired(realm); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + persister = session.getProvider(UserSessionPersisterProvider.class); + + // assert session0 is the only session found + Assert.assertNotNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId())); + Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[1].getId())); + Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[2].getId())); + + Assert.assertEquals(1, persister.getUserSessionsCount(true)); + + // Expire everything and assert nothing found + Time.setOffset(7000000); + + persister.removeExpired(realm); + }); + + inComittedTransaction(session -> { + RealmModel realm = session.realms().getRealm(realmId); + sessionManager = new UserSessionManager(session); + persister = session.getProvider(UserSessionPersisterProvider.class); + + for (String userSessionId : offlineSessions.keySet()) { + Assert.assertNull(sessionManager.findOfflineUserSession(realm, userSessionId)); + } + Assert.assertEquals(0, persister.getUserSessionsCount(true)); + }); + + } finally { + Time.setOffset(0); + kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent()); + if (timer != null) { + timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); + } + + InfinispanTestUtil.revertTimeService(kcSession); + } + } + + private static Set createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel + userSession) { + Set offlineSessions = new HashSet<>(); + UserSessionManager localManager = new UserSessionManager(session); + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { + localManager.createOrUpdateOfflineSession(clientSession, userSession); + offlineSessions.add(clientSession.getClient().getId()); + } + + return offlineSessions; + } +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java index 0db5599c09..3dc602ed71 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java @@ -19,6 +19,11 @@ package org.keycloak.testsuite.model.parameters; import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; import org.keycloak.connections.infinispan.InfinispanConnectionSpi; +import org.keycloak.models.session.UserSessionPersisterSpi; +import org.keycloak.models.sessions.infinispan.InfinispanUserLoginFailureProviderFactory; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; +import org.keycloak.sessions.StickySessionEncoderProviderFactory; +import org.keycloak.sessions.StickySessionEncoderSpi; import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.models.cache.CacheRealmProviderSpi; import org.keycloak.models.cache.CacheUserProviderSpi; @@ -28,6 +33,8 @@ import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; import org.keycloak.testsuite.model.Config; import com.google.common.collect.ImmutableSet; +import org.keycloak.timer.TimerProviderFactory; + import java.util.Set; /** @@ -40,6 +47,8 @@ public class Infinispan extends KeycloakModelParameters { .add(CacheRealmProviderSpi.class) .add(CacheUserProviderSpi.class) .add(InfinispanConnectionSpi.class) + .add(StickySessionEncoderSpi.class) + .add(UserSessionPersisterSpi.class) .build(); @@ -48,6 +57,10 @@ public class Infinispan extends KeycloakModelParameters { .add(InfinispanClusterProviderFactory.class) .add(InfinispanConnectionProviderFactory.class) .add(InfinispanUserCacheProviderFactory.class) + .add(InfinispanUserSessionProviderFactory.class) + .add(InfinispanUserLoginFailureProviderFactory.class) + .add(StickySessionEncoderProviderFactory.class) + .add(TimerProviderFactory.class) .build(); @Override diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java index 733db26e3f..0af7667a8d 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java @@ -25,6 +25,8 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionPr import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi; import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory; import org.keycloak.events.jpa.JpaEventStoreProviderFactory; +import org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory; +import org.keycloak.models.session.UserSessionPersisterSpi; import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.models.dblock.DBLockSpi; import org.keycloak.models.jpa.JpaClientProviderFactory; @@ -51,6 +53,7 @@ public class Jpa extends KeycloakModelParameters { .add(JpaConnectionSpi.class) .add(JpaUpdaterSpi.class) .add(LiquibaseConnectionSpi.class) + .add(UserSessionPersisterSpi.class) .build(); @@ -68,6 +71,7 @@ public class Jpa extends KeycloakModelParameters { .add(JpaUserProviderFactory.class) .add(LiquibaseConnectionProviderFactory.class) .add(LiquibaseDBLockProviderFactory.class) + .add(JpaUserSessionPersisterProviderFactory.class) .build(); public Jpa() { diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java index 43debcd43d..94a3bcc1d5 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java @@ -16,10 +16,15 @@ */ package org.keycloak.testsuite.model.parameters; +import org.keycloak.models.UserLoginFailureSpi; +import org.keycloak.models.UserSessionSpi; +import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory; +import org.keycloak.models.map.userSession.MapUserSessionProviderFactory; import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.models.map.client.MapClientProviderFactory; import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory; import org.keycloak.models.map.group.MapGroupProviderFactory; +import org.keycloak.models.map.realm.MapRealmProviderFactory; import org.keycloak.models.map.role.MapRoleProviderFactory; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageSpi; @@ -45,9 +50,12 @@ public class Map extends KeycloakModelParameters { .add(MapClientProviderFactory.class) .add(MapClientScopeProviderFactory.class) .add(MapGroupProviderFactory.class) + .add(MapRealmProviderFactory.class) .add(MapRoleProviderFactory.class) .add(MapUserProviderFactory.class) .add(MapStorageProvider.class) + .add(MapUserSessionProviderFactory.class) + .add(MapUserLoginFailureProviderFactory.class) .build(); public Map() { @@ -59,8 +67,11 @@ public class Map extends KeycloakModelParameters { cf.spi("client").defaultProvider(MapClientProviderFactory.PROVIDER_ID) .spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID) .spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID) + .spi("realm").defaultProvider(MapRealmProviderFactory.PROVIDER_ID) .spi("role").defaultProvider(MapRoleProviderFactory.PROVIDER_ID) .spi("user").defaultProvider(MapUserProviderFactory.PROVIDER_ID) + .spi(UserSessionSpi.NAME).defaultProvider(MapUserSessionProviderFactory.PROVIDER_ID) + .spi(UserLoginFailureSpi.NAME).defaultProvider(MapUserLoginFailureProviderFactory.PROVIDER_ID) ; } } diff --git a/testsuite/model/src/test/resources/log4j.properties b/testsuite/model/src/test/resources/log4j.properties index 41ffffd503..748025e34e 100644 --- a/testsuite/model/src/test/resources/log4j.properties +++ b/testsuite/model/src/test/resources/log4j.properties @@ -19,7 +19,7 @@ log4j.rootLogger=info, keycloak log4j.appender.keycloak=org.apache.log4j.ConsoleAppender log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout -keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n +keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %m%n log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern} # Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug ) @@ -42,4 +42,7 @@ log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=debug # Enable to log short stack traces for log entries enabled with StackUtil.getShortStackTrace() calls -# log4j.logger.org.keycloak.STACK_TRACE=trace +#log4j.logger.org.keycloak.models.map=trace +#log4j.logger.org.keycloak.models.map.transaction=debug +# +#log4j.logger.org.keycloak.STACK_TRACE=trace diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index 149c7ae81b..57226971af 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -38,6 +38,14 @@ "provider": "${keycloak.authSession.provider:infinispan}" }, + "userSessions": { + "provider": "${keycloak.userSession.provider:infinispan}" + }, + + "loginFailure": { + "provider": "${keycloak.loginFailure.provider:infinispan}" + }, + "mapStorage": { "provider": "${keycloak.mapStorage.provider:concurrenthashmap}", "concurrenthashmap": {