diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5402375bb8..c8149dd031 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -303,6 +303,63 @@ jobs: with: job-id: jdk-integration-tests-${{ matrix.os }}-${{ matrix.dist }}-${{ matrix.version }} + persistent-sessions-tests: + name: Persistent Sessions IT + needs: [build, conditional] + if: needs.conditional.outputs.ci-store == 'true' + runs-on: ubuntu-latest + timeout-minutes: 150 + steps: + - uses: actions/checkout@v4 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Run base tests + run: | + TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh persistent-sessions` + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dauth.server.features=persistent-user-sessions,persistent-user-sessions-no-cache -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Store IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: store-integration-tests-${{ matrix.db }} + + - name: EC2 Maven Logs + if: failure() + uses: actions/upload-artifact@v3 + with: + name: store-it-mvn-logs + path: .github/scripts/ansible/files + + - name: Delete Aurora EC2 Instance + if: ${{ always() && matrix.db == 'aurora-postgres' }} + working-directory: .github/scripts/ansible + run: | + export CLUSTER_NAME=${{ steps.aurora-tests.outputs.ec2_cluster }} + ./aws_ec2.sh delete ${{ steps.aurora-init.outputs.region }} + + - name: Delete Aurora DB + if: ${{ always() && matrix.db == 'aurora-postgres' }} + uses: ./.github/actions/aurora-delete-database + with: + name: ${{ steps.aurora-init.outputs.name }} + region: ${{ steps.aurora-init.outputs.region }} + store-integration-tests: name: Store IT needs: [build, conditional] @@ -757,6 +814,7 @@ jobs: - quarkus-integration-tests - jdk-integration-tests - store-integration-tests + - persistent-sessions-tests - store-model-tests - clustering-integration-tests - fips-unit-tests diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 9172652e8e..a7c85b79ee 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -109,6 +109,9 @@ public class Profile { HOSTNAME_V1("Hostname Options V1", Type.DEFAULT), //HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2), + PERSISTENT_USER_SESSIONS("Persistent online user sessions across restarts and upgrades", Type.EXPERIMENTAL), + PERSISTENT_USER_SESSIONS_NO_CACHE("No caching for online user sessions when they are persisted", Type.EXPERIMENTAL), + OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL), DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL), 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 4e1ed8bee3..cf44e6b244 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 @@ -32,10 +32,9 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.ClientSessionUpdateTask; import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; import org.keycloak.models.sessions.infinispan.changes.Tasks; -import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask; import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshChecker; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + import java.util.UUID; /** @@ -44,14 +43,14 @@ import java.util.UUID; public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel { private final KeycloakSession kcSession; - private final InfinispanUserSessionProvider provider; + private final SessionRefreshStore provider; private AuthenticatedClientSessionEntity entity; private final ClientModel client; private final InfinispanChangelogBasedTransaction clientSessionUpdateTx; private UserSessionModel userSession; private boolean offline; - public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider, + public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, SessionRefreshStore provider, AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession, InfinispanChangelogBasedTransaction clientSessionUpdateTx, boolean offline) { if (userSession == null) { 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 de604041fc..442d861c6a 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 @@ -90,7 +90,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream; /** * @author Stian Thorgersen */ -public class InfinispanUserSessionProvider implements UserSessionProvider { +public class InfinispanUserSessionProvider implements UserSessionProvider, SessionRefreshStore { private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class); @@ -176,15 +176,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return offline ? offlineClientSessionTx : clientSessionTx; } - protected CrossDCLastSessionRefreshStore getLastSessionRefreshStore() { + @Override + public CrossDCLastSessionRefreshStore getLastSessionRefreshStore() { return lastSessionRefreshStore; } - protected CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() { + @Override + public CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() { return offlineLastSessionRefreshStore; } - protected PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() { + @Override + public PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() { return persisterLastSessionRefreshStore; } @@ -244,7 +247,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return adapter; } - void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + static void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { entity.setRealmId(realm.getId()); entity.setUser(user.getId()); entity.setLoginUsername(loginUsername); 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 7bc470a1a5..9f79f4c733 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 @@ -23,6 +23,7 @@ import org.infinispan.persistence.remote.RemoteStore; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.Profile; import org.keycloak.common.util.Environment; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; @@ -94,13 +95,29 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider private InfinispanKeyGenerator keyGenerator; @Override - public InfinispanUserSessionProvider create(KeycloakSession session) { + public UserSessionProvider create(KeycloakSession session) { InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); Cache> cache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); Cache> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); Cache> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); Cache> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + return new PersistentUserSessionProvider( + session, + remoteCacheInvoker, + lastSessionRefreshStore, + offlineLastSessionRefreshStore, + persisterLastSessionRefreshStore, + keyGenerator, + cache, + offlineSessionsCache, + clientSessionCache, + offlineClientSessionsCache, + this::deriveOfflineSessionCacheEntryLifespanMs, + this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs + ); + } return new InfinispanUserSessionProvider( session, remoteCacheInvoker, @@ -148,8 +165,15 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider } else if (event instanceof UserModel.UserRemovedEvent) { UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; - InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId()); - provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser()); + UserSessionProvider provider1 = userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId()); + if (provider1 instanceof InfinispanUserSessionProvider) { + ((InfinispanUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser()); + } else if (provider1 instanceof PersistentUserSessionProvider) { + ((PersistentUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser()); + } else { + throw new IllegalStateException("Unknown provider type: " + provider1.getClass()); + } + } else if (event instanceof ResetTimeOffsetEvent) { if (persisterLastSessionRefreshStore != null) { persisterLastSessionRefreshStore.reset(); @@ -212,6 +236,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) { if (provider instanceof InfinispanUserSessionProvider) { ((InfinispanUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId()); + } else if (provider instanceof PersistentUserSessionProvider) { + ((PersistentUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId()); } } @@ -224,6 +250,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider protected void eventReceived(KeycloakSession session, UserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) { if (provider instanceof InfinispanUserSessionProvider) { ((InfinispanUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid()); + } else if (provider instanceof PersistentUserSessionProvider) { + ((PersistentUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid()); } } @@ -236,6 +264,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) { if (provider instanceof InfinispanUserSessionProvider) { ((InfinispanUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId()); + } else if (provider instanceof PersistentUserSessionProvider) { + ((PersistentUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId()); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java new file mode 100755 index 0000000000..936811e442 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java @@ -0,0 +1,1128 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.exceptions.HotRodClientException; +import org.infinispan.commons.api.BasicCache; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; +import org.keycloak.common.util.Retry; +import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanUtil; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; +import org.keycloak.models.OfflineUserSessionModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.light.LightweightUserAdapter; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.changes.ClientSessionPersistentChangelogBasedTransaction; +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.changes.UserSessionPersistentChangelogBasedTransaction; +import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStore; +import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; +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.SessionPredicate; +import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate; +import org.keycloak.models.sessions.infinispan.util.FuturesHelper; +import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; +import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; +import org.keycloak.models.utils.UserModelDelegate; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +/** + * @author Stian Thorgersen + */ +public class PersistentUserSessionProvider implements UserSessionProvider, SessionRefreshStore { + + private static final Logger log = Logger.getLogger(PersistentUserSessionProvider.class); + + protected final KeycloakSession session; + + protected final Cache> sessionCache; + protected final Cache> offlineSessionCache; + protected final Cache> clientSessionCache; + protected final Cache> offlineClientSessionCache; + + protected final UserSessionPersistentChangelogBasedTransaction sessionTx; + protected final UserSessionPersistentChangelogBasedTransaction offlineSessionTx; + protected final ClientSessionPersistentChangelogBasedTransaction clientSessionTx; + protected final ClientSessionPersistentChangelogBasedTransaction offlineClientSessionTx; + + protected final SessionEventsSenderTransaction clusterEventsSenderTx; + + protected final CrossDCLastSessionRefreshStore lastSessionRefreshStore; + protected final CrossDCLastSessionRefreshStore offlineLastSessionRefreshStore; + protected final PersisterLastSessionRefreshStore persisterLastSessionRefreshStore; + + protected final RemoteCacheInvoker remoteCacheInvoker; + protected final InfinispanKeyGenerator keyGenerator; + + protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster; + + protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster; + + public PersistentUserSessionProvider(KeycloakSession session, + RemoteCacheInvoker remoteCacheInvoker, + CrossDCLastSessionRefreshStore lastSessionRefreshStore, + CrossDCLastSessionRefreshStore offlineLastSessionRefreshStore, + PersisterLastSessionRefreshStore persisterLastSessionRefreshStore, + InfinispanKeyGenerator keyGenerator, + Cache> sessionCache, + Cache> offlineSessionCache, + Cache> clientSessionCache, + Cache> offlineClientSessionCache, + SessionFunction offlineSessionCacheEntryLifespanAdjuster, + SessionFunction offlineClientSessionCacheEntryLifespanAdjuster) { + if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + throw new IllegalStateException("Persistent user sessions are not enabled"); + } + + this.session = session; + + this.sessionCache = sessionCache; + this.clientSessionCache = clientSessionCache; + this.offlineSessionCache = offlineSessionCache; + this.offlineClientSessionCache = offlineClientSessionCache; + + this.sessionTx = new UserSessionPersistentChangelogBasedTransaction(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs, false); + this.offlineSessionTx = new UserSessionPersistentChangelogBasedTransaction(session, offlineSessionCache, remoteCacheInvoker, offlineSessionCacheEntryLifespanAdjuster, SessionTimeouts::getOfflineSessionMaxIdleMs, true); + + this.clientSessionTx = new ClientSessionPersistentChangelogBasedTransaction(session, clientSessionCache, remoteCacheInvoker, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs, false, keyGenerator, sessionTx); + this.offlineClientSessionTx = new ClientSessionPersistentChangelogBasedTransaction(session, offlineClientSessionCache, remoteCacheInvoker, offlineClientSessionCacheEntryLifespanAdjuster, SessionTimeouts::getOfflineClientSessionMaxIdleMs, true, keyGenerator, offlineSessionTx); + + this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); + + this.lastSessionRefreshStore = lastSessionRefreshStore; + this.offlineLastSessionRefreshStore = offlineLastSessionRefreshStore; + this.persisterLastSessionRefreshStore = persisterLastSessionRefreshStore; + this.remoteCacheInvoker = remoteCacheInvoker; + this.keyGenerator = keyGenerator; + this.offlineSessionCacheEntryLifespanAdjuster = offlineSessionCacheEntryLifespanAdjuster; + this.offlineClientSessionCacheEntryLifespanAdjuster = offlineClientSessionCacheEntryLifespanAdjuster; + + session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); + session.getTransactionManager().enlistAfterCompletion(sessionTx); + session.getTransactionManager().enlistAfterCompletion(offlineSessionTx); + session.getTransactionManager().enlistAfterCompletion(clientSessionTx); + session.getTransactionManager().enlistAfterCompletion(offlineClientSessionTx); + } + + protected Cache> getCache(boolean offline) { + return offline ? offlineSessionCache : sessionCache; + } + + protected UserSessionPersistentChangelogBasedTransaction getTransaction(boolean offline) { + return offline ? offlineSessionTx : sessionTx; + } + + protected Cache> getClientSessionCache(boolean offline) { + return offline ? offlineClientSessionCache : clientSessionCache; + } + + protected ClientSessionPersistentChangelogBasedTransaction getClientSessionTransaction(boolean offline) { + return offline ? offlineClientSessionTx : clientSessionTx; + } + + @Override + public CrossDCLastSessionRefreshStore getLastSessionRefreshStore() { + return lastSessionRefreshStore; + } + + @Override + public CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() { + return offlineLastSessionRefreshStore; + } + + @Override + public PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() { + return persisterLastSessionRefreshStore; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + + @Override + public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { + final UUID clientSessionId; + if (userSession.isOffline()) { + clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache); + } else { + clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSession.getId(), client.getId()); + } + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); + entity.setRealmId(realm.getId()); + entity.setClientId(client.getId()); + entity.setUserSessionId(userSession.getId()); + entity.setTimestamp(Time.currentTime()); + entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp())); + entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(userSession.getStarted())); + if (userSession.isRememberMe()) { + entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true"); + } + + UserSessionPersistentChangelogBasedTransaction userSessionUpdateTx = getTransaction(false); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(false); + AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, clientSessionUpdateTx, false); + + if (Profile.isFeatureEnabled(Feature.PERSISTENT_USER_SESSIONS_NO_CACHE)) { + if (userSession.isOffline()) { + // If this is an offline session, and the referred online session doesn't exist anymore, don't register the client session in the transaction. + // Instead keep it transient and it will be added to the offline session only afterward. This is expected by SessionTimeoutsTest.testOfflineUserClientIdleTimeoutSmallerThanSessionOneRefresh. + if (userSessionUpdateTx.get(realm, userSession.getId()) == null) { + return adapter; + } + } + } + + // For now, the clientSession is considered transient in case that userSession was transient + UserSessionModel.SessionPersistenceState persistenceState = userSession.getPersistenceState() != null ? + userSession.getPersistenceState() : UserSessionModel.SessionPersistenceState.PERSISTENT; + + SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); + clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState); + + SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId); + userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask); + + return adapter; + } + + @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) { + if (id == null) { + id = keyGenerator.generateKeyString(session, sessionCache); + } + + UserSessionEntity entity = new UserSessionEntity(); + entity.setId(id); + updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); + + SessionUpdateTask createSessionTask = Tasks.addIfAbsentSync(); + sessionTx.addTask(id, createSessionTask, entity, persistenceState); + + UserSessionAdapter adapter = user instanceof LightweightUserAdapter + ? wrap(realm, entity, false, user) + : wrap(realm, entity, false); + adapter.setPersistenceState(persistenceState); + return adapter; + } + + void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + entity.setRealmId(realm.getId()); + entity.setUser(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); + } + + @Override + public UserSessionModel getUserSession(RealmModel realm, String id) { + return getUserSession(realm, id, false); + } + + protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { + UserSessionPersistentChangelogBasedTransaction tx = getTransaction(offline); + SessionEntityWrapper entityWrapper = tx.get(realm, id); + if (entityWrapper == null) { + return null; + } + + UserSessionEntity entity = entityWrapper.getEntity(); + if (entity.getRealmId().equals(realm.getId())) { + return wrap(realm, entity, offline); + } + + return null; + } + + private UserSessionEntity getUserSessionFromTx(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) { + SessionEntityWrapper userSessionEntitySessionEntityWrapper = getTransaction(offline).get(realm, persistentUserSession.getId()); + if (userSessionEntitySessionEntityWrapper != null) { + return userSessionEntitySessionEntityWrapper.getEntity(); + } + return null; + } + + private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, boolean offline) { + UserSessionPersistentChangelogBasedTransaction tx = getTransaction(offline); + SessionEntityWrapper entityWrapper = tx.get(realm, id); + if (entityWrapper == null) { + return null; + } + + UserSessionEntity entity = entityWrapper.getEntity(); + if (!entity.getRealmId().equals(realm.getId())) { + return null; + } + return entity; + } + + private Stream getUserSessionsFromPersistenceProviderStream(RealmModel realm, UserModel user, boolean offline) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + return persister.loadUserSessionsStream(realm, user, offline, 0, null) + .map(persistentUserSession -> (UserSessionModel) getUserSession(realm, persistentUserSession.getId(), offline)) + .filter(Objects::nonNull); + } + + + protected Stream getUserSessionsStream(RealmModel realm, UserSessionPredicate predicate, boolean offline) { + // fetch the offline user-sessions from the persistence provider + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + if (predicate.getUserId() != null) { + UserModel user; + if (LightweightUserAdapter.isLightweightUser(predicate.getUserId())) { + user = new UserModelDelegate(null) { + @Override + public String getId() { + return predicate.getUserId(); + } + }; + } else { + user = session.users().getUserById(realm, predicate.getUserId()); + } + if (user != null) { + return persister.loadUserSessionsStream(realm, user, offline, 0, null) + .filter(predicate.toModelPredicate()) + .map(s -> (UserSessionModel) getUserSession(realm, s.getId(), offline)) + .filter(Objects::nonNull); + } else { + return Stream.empty(); + } + } + + if (predicate.getBrokerUserId() != null) { + String[] idpAliasSessionId = predicate.getBrokerUserId().split("\\."); + + Map attributes = new HashMap<>(); + attributes.put(UserModel.IDP_ALIAS, idpAliasSessionId[0]); + attributes.put(UserModel.IDP_USER_ID, idpAliasSessionId[1]); + + UserProvider userProvider = session.getProvider(UserProvider.class); + UserModel userModel = userProvider.searchForUserStream(realm, attributes, 0, null).findFirst().orElse(null); + return userModel != null ? + persister.loadUserSessionsStream(realm, userModel, offline, 0, null) + .filter(predicate.toModelPredicate()) + .map(s -> (UserSessionModel) getUserSession(realm, s.getId(), offline)) + .filter(Objects::nonNull) : + Stream.empty(); + } + + if (predicate.getClient() != null) { + ClientModel client = session.clients().getClientById(realm, predicate.getClient()); + return persister.loadUserSessionsStream(realm, client, offline, 0, null) + .filter(predicate.toModelPredicate()) + .map(s -> (UserSessionModel) getUserSession(realm, s.getId(), offline)) + .filter(Objects::nonNull); + } + + if (predicate.getBrokerSessionId() != null && !offline) { + // we haven't yet migrated the old offline entries, so they don't have a brokerSessionId yet + return Stream.of(persister.loadUserSessionsStreamByBrokerSessionId(realm, predicate.getBrokerSessionId(), offline)) + .filter(predicate.toModelPredicate()) + .map(s -> (UserSessionModel) getUserSession(realm, s.getId(), offline)) + .filter(Objects::nonNull); + } + + throw new ModelException("For offline sessions, only lookup by userId, brokerUserId and client is supported"); + } + + @Override + public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, String clientSessionId, boolean offline) { + if (clientSessionId == null) { + return null; + } + + UUID clientSessionUUID = UUID.fromString(clientSessionId); + ClientSessionPersistentChangelogBasedTransaction clientTx = getClientSessionTransaction(offline); + + SessionEntityWrapper clientSessionEntity = clientTx.get(client.getRealm(), client, userSession, clientSessionUUID); + if (clientSessionEntity != null) { + return new AuthenticatedClientSessionAdapter(session, this, clientSessionEntity.getEntity(), client, userSession, clientTx, offline); + } + + return null; + } + + + @Override + public Stream getUserSessionsStream(final RealmModel realm, UserModel user) { + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), false); + } + + @Override + public Stream getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) { + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), false); + } + + @Override + public UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { + // TODO: consider returning a list as it is not guaranteed to be unique, and might be active for different users + return this.getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), false) + .findFirst().orElse(null); + } + + @Override + public Stream getUserSessionsStream(RealmModel realm, ClientModel client) { + return getUserSessionsStream(realm, client, -1, -1); + } + + @Override + public Stream getUserSessionsStream(RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults) { + return getUserSessionsStream(realm, client, firstResult, maxResults, false); + } + + protected Stream getUserSessionsStream(final RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults, final boolean offline) { + UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(client.getId()); + + return paginatedStream(getUserSessionsStream(realm, predicate, offline) + .sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh)), firstResult, maxResults); + } + + @Override + public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate predicate) { + UserSessionModel userSession = getUserSession(realm, id, offline); + if (userSession == null) { + return null; + } + + // We have userSession, which passes predicate. No need for remote lookup. + if (predicate.test(userSession)) { + log.debugf("getUserSessionWithPredicate(%s): found in local cache", id); + return userSession; + } + + if (Profile.isFeatureEnabled(Feature.PERSISTENT_USER_SESSIONS_NO_CACHE)) { + return null; + } + + // Try lookup userSession from remoteCache + Cache> cache = getCache(offline); + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + + if (remoteCache != null) { + SessionEntityWrapper remoteSessionEntityWrapper = (SessionEntityWrapper) remoteCache.get(id); + if (remoteSessionEntityWrapper != null) { + UserSessionEntity remoteSessionEntity = remoteSessionEntityWrapper.getEntity(); + log.debugf("getUserSessionWithPredicate(%s): remote cache contains session entity %s", id, remoteSessionEntity); + + UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline); + if (predicate.test(remoteSessionAdapter)) { + + InfinispanChangelogBasedTransaction tx = getTransaction(offline); + + // Remote entity contains our predicate. Update local cache with the remote entity + SessionEntityWrapper sessionWrapper = remoteSessionEntity.mergeRemoteEntityWithLocalEntity(tx.get(id)); + + // Replace entity just in ispn cache. Skip remoteStore + cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES) + .replace(id, sessionWrapper); + + tx.reloadEntityInCurrentTransaction(realm, id, sessionWrapper); + + // Recursion. We should have it locally now + return getUserSessionWithPredicate(realm, id, offline, predicate); + } else { + log.debugf("getUserSessionWithPredicate(%s): found, but predicate doesn't pass", id); + + return null; + } + } else { + log.debugf("getUserSessionWithPredicate(%s): not found", id); + + // Session not available on remoteCache. Was already removed there. So removing locally too. + // TODO: Can be optimized to skip calling remoteCache.remove + removeUserSession(realm, userSession); + + return null; + } + } else { + + log.debugf("getUserSessionWithPredicate(%s): remote cache not available", id); + + return null; + } + } + + + @Override + public long getActiveUserSessions(RealmModel realm, ClientModel client) { + return getUserSessionsCount(realm, client, false); + } + + @Override + public Map getActiveClientSessionStats(RealmModel realm, boolean offline) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + return persister.getUserSessionsCountsByClients(realm, offline); + } + + protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { + // fetch the actual offline user session count from the database + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + return persister.getUserSessionsCount(realm, client, offline); + } + + @Override + public void removeUserSession(RealmModel realm, UserSessionModel session) { + UserSessionEntity entity = getUserSessionEntity(realm, session, false); + if (entity != null) { + removeUserSession(entity, false); + } + } + + @Override + public void removeUserSessions(RealmModel realm, UserModel user) { + removeUserSessions(realm, user, false); + } + + protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { + UserSessionPredicate.create(realm.getId()).user(user.getId()); + getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), offline) + .forEach(s -> removeUserSession(realm, s)); + } + + public void removeAllExpired() { + // Rely on expiration of cache entries provided by infinispan. Just expire entries from persister is needed + // TODO: Avoid iteration over all realms here (Details in the KEYCLOAK-16802) + session.realms().getRealmsStream().forEach(this::removeExpired); + + } + + @Override + public void removeExpired(RealmModel realm) { + // Rely on expiration of cache entries provided by infinispan. Nothing needed here besides calling persister + session.getProvider(UserSessionPersisterProvider.class).removeExpired(realm); + } + + @Override + public void removeUserSessions(RealmModel realm) { + // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions. + clusterEventsSenderTx.addEvent( + RemoveUserSessionsEvent.createEvent(RemoveUserSessionsEvent.class, InfinispanUserSessionProviderFactory.REMOVE_USER_SESSIONS_EVENT, session, realm.getId(), true), + ClusterProvider.DCNotify.LOCAL_DC_ONLY); + + session.getProvider(UserSessionPersisterProvider.class).removeUserSessions(realm, false); + } + + protected void onRemoveUserSessionsEvent(String realmId) { + removeLocalUserSessions(realmId, false); + removeLocalUserSessions(realmId, true); + } + + // public for usage in the testsuite + public void removeLocalUserSessions(String realmId, boolean offline) { + FuturesHelper futures = new FuturesHelper(); + + Cache> cache = getCache(offline); + Cache> localCache = CacheDecorators.localCache(cache); + Cache> clientSessionCache = getClientSessionCache(offline); + Cache> localClientSessionCache = CacheDecorators.localCache(clientSessionCache); + + Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(localCache); + + final AtomicInteger userSessionsSize = new AtomicInteger(); + + localCacheStoreIgnore + .entrySet() + .stream() + .filter(SessionPredicate.create(realmId)) + .map(Mappers.userSessionEntity()) + .forEach(new Consumer() { + + @Override + public void accept(UserSessionEntity userSessionEntity) { + userSessionsSize.incrementAndGet(); + + // Remove session from remoteCache too. Use removeAsync for better perf + Future future = localCache.removeAsync(userSessionEntity.getId()); + futures.addTask(future); + userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { + Future f = localClientSessionCache.removeAsync(clientSessionId); + futures.addTask(f); + }); + } + + }); + + + futures.waitForAllToFinish(); + + log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) userSessionsSize.get(), realmId, offline); + } + + @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. + clusterEventsSenderTx.addEvent( + RealmRemovedSessionEvent.createEvent(RealmRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.REALM_REMOVED_SESSION_EVENT, session, realm.getId(), true), + ClusterProvider.DCNotify.LOCAL_DC_ONLY); + + UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class); + if (sessionsPersister != null) { + sessionsPersister.onRealmRemoved(realm); + } + } + + protected void onRealmRemovedEvent(String realmId) { + removeLocalUserSessions(realmId, true); + removeLocalUserSessions(realmId, false); + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { +// clusterEventsSenderTx.addEvent( +// ClientRemovedSessionEvent.createEvent(ClientRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.CLIENT_REMOVED_SESSION_EVENT, session, realm.getId(), true), +// ClusterProvider.DCNotify.LOCAL_DC_ONLY); + UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class); + if (sessionsPersister != null) { + sessionsPersister.onClientRemoved(realm, client); + } + } + + protected void onClientRemovedEvent(String realmId, String clientUuid) { + // Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly. + } + + + protected void onUserRemoved(RealmModel realm, UserModel user) { + removeUserSessions(realm, user, true); + removeUserSessions(realm, user, false); + + UserSessionPersisterProvider persisterProvider = session.getProvider(UserSessionPersisterProvider.class); + if (persisterProvider != null) { + persisterProvider.onUserRemoved(realm, user); + } + } + + @Override + public void close() { + } + + @Override + public int getStartupTime(RealmModel realm) { + // TODO: take realm.getNotBefore() into account? + return session.getProvider(ClusterProvider.class).getClusterStartupTime(); + } + + protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) { + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + sessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync())); + SessionUpdateTask removeTask = Tasks.removeSync(); + userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask); + } + + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline, UserModel user) { + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + + if (entity == null) { + return null; + } + + return new UserSessionAdapter(session, user, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline); + } + + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { + UserModel user = null; + if (Profile.isFeatureEnabled(Feature.TRANSIENT_USERS) && entity.getNotes().containsKey(SESSION_NOTE_LIGHTWEIGHT_USER)) { + LightweightUserAdapter lua = LightweightUserAdapter.fromString(session, realm, entity.getNotes().get(SESSION_NOTE_LIGHTWEIGHT_USER)); + final UserSessionAdapter us = wrap(realm, entity, offline, lua); + lua.setUpdateHandler(lua1 -> { + if (lua == lua1) { // Ensure there is no conflicting user model, only the latest lightweight user can be used + us.setNote(SESSION_NOTE_LIGHTWEIGHT_USER, lua1.serialize()); + } + }); + return us; + } + + user = session.users().getUserById(realm, entity.getUser()); + + if (user == null) { + return null; + } + + return wrap(realm, entity, offline, user); + } + + UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) { + if (userSession instanceof UserSessionAdapter) { + if (!userSession.getRealm().equals(realm)) { + return null; + } + return ((UserSessionAdapter) userSession).getEntity(); + } else { + return getUserSessionEntity(realm, userSession.getId(), offline); + } + } + + + @Override + public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { + UserSessionEntity entity = createUserSessionEntityInstance(userSession); + + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(true); + + SessionUpdateTask importTask = Tasks.addIfAbsentSync(); + userSessionUpdateTx.addTask(userSession.getId(), importTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); + + UserSessionAdapter offlineUserSession = wrap(userSession.getRealm(), entity, true); + + // started and lastSessionRefresh set to current time + int currentTime = Time.currentTime(); + offlineUserSession.getEntity().setStarted(currentTime); + offlineUserSession.getEntity().setLastSessionRefresh(currentTime); + + return offlineUserSession; + } + + @Override + public UserSessionAdapter getOfflineUserSession(RealmModel realm, String userSessionId) { + return getUserSession(realm, userSessionId, true); + } + + @Override + public Stream getOfflineUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) { + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), true); + } + + @Override + public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) { + UserSessionEntity userSessionEntity = getUserSessionEntity(realm, userSession, true); + if (userSessionEntity != null) { + removeUserSession(userSessionEntity, true); + } + } + + @Override + public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) { + UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : + getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); + + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(true); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(true); + AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, true, false); + + // update timestamp to current time + offlineClientSession.setTimestamp(Time.currentTime()); + offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(offlineClientSession.getTimestamp())); + offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted())); + + return offlineClientSession; + } + + @Override + public Stream getOfflineUserSessionsStream(RealmModel realm, UserModel user) { + return getUserSessionsFromPersistenceProviderStream(realm, user, true); + } + + @Override + public long getOfflineSessionsCount(RealmModel realm, ClientModel client) { + return getUserSessionsCount(realm, client, true); + } + + @Override + public Stream getOfflineUserSessionsStream(RealmModel realm, ClientModel client, Integer first, Integer max) { + return getUserSessionsStream(realm, client, first, max, true); + } + + + @Override + public void importUserSessions(Collection persistentUserSessions, boolean offline) { + if (persistentUserSessions == null || persistentUserSessions.isEmpty()) { + return; + } + + Map> clientSessionsById = new HashMap<>(); + + Map> sessionsById = persistentUserSessions.stream() + .map((UserSessionModel persistentUserSession) -> { + + UserSessionEntity userSessionEntityToImport = createUserSessionEntityInstance(persistentUserSession); + + for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { + String clientUUID = entry.getKey(); + AuthenticatedClientSessionModel clientSession = entry.getValue(); + AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(userSessionEntityToImport.getId(), clientSession, + userSessionEntityToImport.getRealmId(), clientUUID, offline); + clientSessionToImport.setUserSessionId(userSessionEntityToImport.getId()); + + // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value + clientSessionToImport.setTimestamp(userSessionEntityToImport.getLastSessionRefresh()); + + clientSessionsById.put(clientSessionToImport.getId(), new SessionEntityWrapper<>(clientSessionToImport)); + + // Update userSession entity with the clientSession + AuthenticatedClientSessionStore clientSessions = userSessionEntityToImport.getAuthenticatedClientSessions(); + clientSessions.put(clientUUID, clientSessionToImport.getId()); + } + + return userSessionEntityToImport; + }) + .map(SessionEntityWrapper::new) + .collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity())); + + // Directly put all entities to the infinispan cache + Cache> cache = CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(getCache(offline)); + + boolean importWithExpiration = sessionsById.size() == 1; + if (importWithExpiration) { + importSessionsWithExpiration(sessionsById, cache, + offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, + offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); + } else { + Retry.executeWithBackoff((int iteration) -> { + cache.putAll(sessionsById); + }, 10, 10); + } + + // put all entities to the remoteCache (if exists) + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + if (remoteCache != null) { + Map> sessionsByIdForTransport = sessionsById.values().stream() + .map(SessionEntityWrapper::forTransport) + .collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity())); + + if (importWithExpiration) { + importSessionsWithExpiration(sessionsByIdForTransport, remoteCache, + offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, + offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); + } else { + Retry.executeWithBackoff((int iteration) -> { + + try { + remoteCache.putAll(sessionsByIdForTransport); + } catch (HotRodClientException re) { + if (log.isDebugEnabled()) { + log.debugf(re, "Failed to put import %d sessions to remoteCache. Iteration '%s'. Will try to retry the task", + sessionsByIdForTransport.size(), iteration); + } + + // Rethrow the exception. Retry will take care of handle the exception and eventually retry the operation. + throw re; + } + + }, 10, 10); + } + } + + // Import client sessions + Cache> clientSessCache = + CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(offline ? offlineClientSessionCache : clientSessionCache); + + if (importWithExpiration) { + importSessionsWithExpiration(clientSessionsById, clientSessCache, + offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, + offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); + } else { + Retry.executeWithBackoff((int iteration) -> { + clientSessCache.putAll(clientSessionsById); + }, 10, 10); + } + + // put all entities to the remoteCache (if exists) + RemoteCache remoteCacheClientSessions = InfinispanUtil.getRemoteCache(clientSessCache); + if (remoteCacheClientSessions != null) { + Map> sessionsByIdForTransport = clientSessionsById.values().stream() + .map(SessionEntityWrapper::forTransport) + .collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity())); + + if (importWithExpiration) { + importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions, + offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, + offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); + } else { + Retry.executeWithBackoff((int iteration) -> { + + try { + remoteCacheClientSessions.putAll(sessionsByIdForTransport); + } catch (HotRodClientException re) { + if (log.isDebugEnabled()) { + log.debugf(re, "Failed to put import %d client sessions to remoteCache. Iteration '%s'. Will try to retry the task", + sessionsByIdForTransport.size(), iteration); + } + + // Rethrow the exception. Retry will take care of handle the exception and eventually retry the operation. + throw re; + } + + }, 10, 10); + } + } + } + + private void importSessionsWithExpiration(Map> sessionsById, + BasicCache cache, SessionFunction lifespanMsCalculator, + SessionFunction maxIdleTimeMsCalculator) { + sessionsById.forEach((id, sessionEntityWrapper) -> { + + T sessionEntity = sessionEntityWrapper.getEntity(); + RealmModel currentRealm = session.realms().getRealm(sessionEntity.getRealmId()); + ClientModel client = sessionEntityWrapper.getClientIfNeeded(currentRealm); + long lifespan = lifespanMsCalculator.apply(currentRealm, client, sessionEntity); + long maxIdle = maxIdleTimeMsCalculator.apply(currentRealm, client, sessionEntity); + + if (lifespan != SessionTimeouts.ENTRY_EXPIRED_FLAG + && maxIdle != SessionTimeouts.ENTRY_EXPIRED_FLAG) { + if (cache instanceof RemoteCache) { + Retry.executeWithBackoff((int iteration) -> { + + try { + cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); + } catch (HotRodClientException re) { + if (log.isDebugEnabled()) { + log.debugf(re, "Failed to put import %d sessions to remoteCache. Iteration '%s'. Will try to retry the task", + sessionsById.size(), iteration); + } + + // Rethrow the exception. Retry will take care of handle the exception and eventually retry the operation. + throw re; + } + + }, 10, 10); + } else { + cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); + } + } + }); + } + + private UserSessionEntity createUserSessionEntityInstance(UserSessionModel userSession) { + UserSessionEntity entity = new UserSessionEntity(); + entity.setId(userSession.getId()); + entity.setRealmId(userSession.getRealm().getId()); + + entity.setAuthMethod(userSession.getAuthMethod()); + entity.setBrokerSessionId(userSession.getBrokerSessionId()); + entity.setBrokerUserId(userSession.getBrokerUserId()); + entity.setIpAddress(userSession.getIpAddress()); + entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes()); + entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore()); + entity.setRememberMe(userSession.isRememberMe()); + entity.setState(userSession.getState()); + if (userSession instanceof OfflineUserSessionModel) { + // this is a hack so that UserModel doesn't have to be available when offline token is imported. + // see related JIRA - KEYCLOAK-5350 and corresponding test + OfflineUserSessionModel oline = (OfflineUserSessionModel) userSession; + entity.setUser(oline.getUserId()); + // NOTE: Hack + // We skip calling entity.setLoginUsername(userSession.getLoginUsername()) + + } else { + entity.setLoginUsername(userSession.getLoginUsername()); + entity.setUser(userSession.getUser().getId()); + } + + entity.setStarted(userSession.getStarted()); + entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + + return entity; + } + + + private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession, + InfinispanChangelogBasedTransaction userSessionUpdateTx, + InfinispanChangelogBasedTransaction clientSessionUpdateTx, + boolean offline, boolean checkExpiration) { + AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(sessionToImportInto.getId(), clientSession, + sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), offline); + entity.setUserSessionId(sessionToImportInto.getId()); + + // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value + entity.setTimestamp(sessionToImportInto.getLastSessionRefresh()); + + if (checkExpiration) { + SessionFunction lifespanChecker = offline + ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs; + SessionFunction idleTimeoutChecker = offline + ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs; + if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG + || lifespanChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) { + return null; + } + } + + final UUID clientSessionId = entity.getId(); + + SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); + clientSessionUpdateTx.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); + + AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); + clientSessions.put(clientSession.getClient().getId(), clientSessionId); + + SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId); + userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask); + + return new AuthenticatedClientSessionAdapter(session, this, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, offline); + } + + + private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession, + String realmId, String clientId, boolean offline) { + final UUID clientSessionId; + if (offline) { + clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache); + } else { + clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSessionId, clientId); + } + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); + entity.setRealmId(realmId); + + entity.setAction(clientSession.getAction()); + entity.setAuthMethod(clientSession.getProtocol()); + + entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes()); + entity.setClientId(clientId); + entity.setRedirectUri(clientSession.getRedirectUri()); + entity.setTimestamp(clientSession.getTimestamp()); + entity.setCurrentRefreshToken(clientSession.getCurrentRefreshToken()); + entity.setCurrentRefreshTokenUseCount(clientSession.getCurrentRefreshTokenUseCount()); + + return entity; + } + + public SessionEntityWrapper wrapPersistentEntity(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) { + UserSessionEntity userSessionEntity = createUserSessionEntityInstance(persistentUserSession); + + if (isUserSessionExpired(realm, userSessionEntity, offline)) { + return null; + } + + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + userSessionUpdateTx.addTask(userSessionEntity.getId(), null, userSessionEntity, UserSessionModel.SessionPersistenceState.PERSISTENT); + + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + + for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { + String clientUUID = entry.getKey(); + AuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(persistentUserSession.getId(), entry.getValue(), + userSessionEntity.getRealmId(), clientUUID, offline); + clientSession.setUserSessionId(userSessionEntity.getId()); + + // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value + // clientSession.setTimestamp(userSessionEntity.getLastSessionRefresh()); + + ClientModel client = session.clients().getClientById(realm, clientSession.getClientId()); + if (isClientSessionExpired(realm, client, clientSession, offline)) { + continue; + } + + // Update userSession entity with the clientSession + AuthenticatedClientSessionStore clientSessions = userSessionEntity.getAuthenticatedClientSessions(); + clientSessions.put(clientUUID, clientSession.getId()); + clientSessionUpdateTx.addTask(clientSession.getId(), null, clientSession, UserSessionModel.SessionPersistenceState.PERSISTENT); + } + + return sessionTx.get(realm, userSessionEntity.getId()); + + } + + private boolean isClientSessionExpired(RealmModel realm, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) { + SessionFunction idleChecker = offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs; + SessionFunction lifetimeChecker = offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs; + return idleChecker.apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifetimeChecker.apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG; + } + + private boolean isUserSessionExpired(RealmModel realm, UserSessionEntity entity, boolean offline) { + SessionFunction idleChecker = offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs; + SessionFunction lifetimeChecker = offline ? SessionTimeouts::getOfflineSessionLifespanMs : SessionTimeouts::getUserSessionLifespanMs; + return idleChecker.apply(realm, null, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifetimeChecker.apply(realm, null, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG; + } + + private static class RegisterClientSessionTask implements SessionUpdateTask { + + private final String clientUuid; + private final UUID clientSessionId; + + public RegisterClientSessionTask(String clientUuid, UUID clientSessionId) { + this.clientUuid = clientUuid; + this.clientSessionId = clientSessionId; + } + + @Override + public void runUpdate(UserSessionEntity session) { + AuthenticatedClientSessionStore clientSessions = session.getAuthenticatedClientSessions(); + clientSessions.put(clientUuid, clientSessionId); + } + + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.REPLACE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + } + + public static UUID createClientSessionUUID(String userSessionId, String clientId) { + return UUID.nameUUIDFromBytes((userSessionId + clientId).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/SessionRefreshStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/SessionRefreshStore.java new file mode 100644 index 0000000000..0b15f6eaea --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/SessionRefreshStore.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStore; +import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore; + +public interface SessionRefreshStore { + CrossDCLastSessionRefreshStore getLastSessionRefreshStore(); + + CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore(); + + PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore(); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index e06408b91f..146873952d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshChecker; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; @@ -43,16 +44,14 @@ import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; -import static org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE; - /** * @author Stian Thorgersen */ -public class UserSessionAdapter implements UserSessionModel { +public class UserSessionAdapter implements UserSessionModel { private final KeycloakSession session; - private final InfinispanUserSessionProvider provider; + private final T provider; private final InfinispanChangelogBasedTransaction userSessionUpdateTx; @@ -68,7 +67,7 @@ public class UserSessionAdapter implements UserSessionModel { private SessionPersistenceState persistenceState; - public UserSessionAdapter(KeycloakSession session, UserModel user, InfinispanUserSessionProvider provider, + public UserSessionAdapter(KeycloakSession session, UserModel user, T provider, InfinispanChangelogBasedTransaction userSessionUpdateTx, InfinispanChangelogBasedTransaction clientSessionUpdateTx, RealmModel realm, UserSessionEntity entity, boolean offline) { @@ -94,7 +93,7 @@ public class UserSessionAdapter implements UserSessionModel { // Check if client still exists ClientModel client = realm.getClientById(key); if (client != null) { - final AuthenticatedClientSessionAdapter clientSession = provider.getClientSession(this, client, value.toString(), offline); + final AuthenticatedClientSessionModel clientSession = provider.getClientSession(this, client, value.toString(), offline); if (clientSession != null) { result.put(key, clientSession); } @@ -330,7 +329,7 @@ public class UserSessionAdapter implements UserSessionModel { @Override public void runUpdate(UserSessionEntity entity) { - provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); + InfinispanUserSessionProvider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); entity.setState(null); entity.getNotes().clear(); @@ -360,7 +359,8 @@ public class UserSessionAdapter implements UserSessionModel { return getId().hashCode(); } - UserSessionEntity getEntity() { + // TODO: This should not be public + public UserSessionEntity getEntity() { return entity; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java new file mode 100644 index 0000000000..943f3d1261 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java @@ -0,0 +1,191 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.infinispan.Cache; +import org.jboss.logging.Logger; +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.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider; +import org.keycloak.models.sessions.infinispan.SessionFunction; +import org.keycloak.models.sessions.infinispan.UserSessionAdapter; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; +import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; +import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class ClientSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction { + + private static final Logger LOG = Logger.getLogger(ClientSessionPersistentChangelogBasedTransaction.class); + private final InfinispanKeyGenerator keyGenerator; + private final UserSessionPersistentChangelogBasedTransaction userSessionTx; + + public ClientSessionPersistentChangelogBasedTransaction(KeycloakSession session, Cache> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction lifespanMsLoader, SessionFunction maxIdleTimeMsLoader, boolean offline, InfinispanKeyGenerator keyGenerator, UserSessionPersistentChangelogBasedTransaction userSessionTx) { + super(session, cache, remoteCacheInvoker, lifespanMsLoader, maxIdleTimeMsLoader, offline); + this.keyGenerator = keyGenerator; + this.userSessionTx = userSessionTx; + } + + public SessionEntityWrapper get(RealmModel realm, ClientModel client, UserSessionModel userSession, UUID key) { + SessionUpdatesList myUpdates = updates.get(key); + if (myUpdates == null) { + SessionEntityWrapper wrappedEntity = cache.get(key); + if (wrappedEntity == null) { + wrappedEntity = getSessionEntityFromPersister(realm, client, userSession); + } + + if (wrappedEntity == null) { + return null; + } + + RealmModel realmFromSession = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId()); + if (!realmFromSession.getId().equals(realm.getId())) { + LOG.warnf("Realm mismatch for session %s. Expected realm %s, but found realm %s", wrappedEntity.getEntity(), realm.getId(), realmFromSession.getId()); + return null; + } + + myUpdates = new SessionUpdatesList<>(realm, wrappedEntity); + updates.put(key, myUpdates); + + return wrappedEntity; + } else { + AuthenticatedClientSessionEntity entity = myUpdates.getEntityWrapper().getEntity(); + + // If entity is scheduled for remove, we don't return it. + boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> { + + return task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE; + + }).findFirst().isPresent(); + + return scheduledForRemove ? null : myUpdates.getEntityWrapper(); + } + } + + private SessionEntityWrapper getSessionEntityFromPersister(RealmModel realm, ClientModel client, UserSessionModel userSession) { + UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class); + AuthenticatedClientSessionModel clientSession = persister.loadClientSession(realm, client, userSession, offline); + + if (clientSession == null) { + return null; + } + + SessionEntityWrapper authenticatedClientSessionEntitySessionEntityWrapper = importClientSession(realm, client, userSession, clientSession); + if (authenticatedClientSessionEntitySessionEntityWrapper == null) { + persister.removeClientSession(userSession.getId(), client.getId(), offline); + } + + return authenticatedClientSessionEntitySessionEntityWrapper; + } + + private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession, + String realmId, String clientId) { + UUID clientSessionId = null; + if (clientSession.getId() != null) { + clientSessionId = UUID.fromString(clientSession.getId()); + } else { + if (offline) { + clientSessionId = keyGenerator.generateKeyUUID(kcSession, cache); + } else { + clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSessionId, clientId); + } + } + + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); + entity.setRealmId(realmId); + + entity.setAction(clientSession.getAction()); + entity.setAuthMethod(clientSession.getProtocol()); + + entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes()); + entity.setClientId(clientId); + entity.setRedirectUri(clientSession.getRedirectUri()); + entity.setTimestamp(clientSession.getTimestamp()); + + return entity; + } + + private SessionEntityWrapper importClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionModel persistentClientSession) { + AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), persistentClientSession, + realm.getId(), client.getId()); + + entity.setUserSessionId(userSession.getId()); + + // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value + entity.setTimestamp(userSession.getLastSessionRefresh()); + + if (maxIdleTimeMsLoader.apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG + || lifespanMsLoader.apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) { + return null; + } + + final UUID clientSessionId = entity.getId(); + + SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); + this.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); + + if (! (userSession instanceof UserSessionAdapter)) { + throw new IllegalStateException("UserSessionModel must be instance of UserSessionAdapter"); + } + + UserSessionAdapter sessionToImportInto = (UserSessionAdapter) userSession; + AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); + clientSessions.put(client.getId(), clientSessionId); + + SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId); + userSessionTx.addTask(sessionToImportInto.getId(), registerClientSessionTask); + + return new SessionEntityWrapper<>(entity); + } + + private static class RegisterClientSessionTask implements SessionUpdateTask { + + private final String clientUuid; + private final UUID clientSessionId; + + public RegisterClientSessionTask(String clientUuid, UUID clientSessionId) { + this.clientUuid = clientUuid; + this.clientSessionId = clientSessionId; + } + + @Override + public void runUpdate(UserSessionEntity session) { + AuthenticatedClientSessionStore clientSessions = session.getAuthenticatedClientSessions(); + clientSessions.put(clientUuid, clientSessionId); + } + + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.REPLACE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/EmbeddedCachesChangesPerformer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/EmbeddedCachesChangesPerformer.java new file mode 100644 index 0000000000..fbd53bc94f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/EmbeddedCachesChangesPerformer.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.infinispan.Cache; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.connections.infinispan.InfinispanUtil; +import org.keycloak.models.sessions.infinispan.CacheDecorators; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class EmbeddedCachesChangesPerformer implements SessionChangesPerformer { + + private static final Logger LOG = Logger.getLogger(EmbeddedCachesChangesPerformer.class); + private final Cache> cache; + private final List changes = new LinkedList<>(); + + public EmbeddedCachesChangesPerformer(Cache> cache) { + this.cache = cache; + } + + private void runOperationInCluster(K key, MergedUpdate task, SessionEntityWrapper sessionWrapper) { + V session = sessionWrapper.getEntity(); + SessionUpdateTask.CacheOperation operation = task.getOperation(session); + + // Don't need to run update of underlying entity. Local updates were already run + //task.runUpdate(session); + + switch (operation) { + case REMOVE: + // Just remove it + CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache) + .withFlags(Flag.IGNORE_RETURN_VALUES) + .remove(key); + break; + case ADD: + CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache) + .withFlags(Flag.IGNORE_RETURN_VALUES) + .put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS); + + LOG.tracef("Added entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs()); + break; + case ADD_IF_ABSENT: + SessionEntityWrapper existing = CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache).putIfAbsent(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS); + if (existing != null) { + LOG.debugf("Existing entity in cache for key: %s . Will update it", key); + + // Apply updates on the existing entity and replace it + task.runUpdate(existing.getEntity()); + + replace(key, task, existing, task.getLifespanMs(), task.getMaxIdleTimeMs()); + } else { + LOG.tracef("Add_if_absent successfully called for entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs()); + } + break; + case REPLACE: + replace(key, task, sessionWrapper, task.getLifespanMs(), task.getMaxIdleTimeMs()); + break; + default: + throw new IllegalStateException("Unsupported state " + operation); + } + + } + + private void replace(K key, MergedUpdate task, SessionEntityWrapper oldVersionEntity, long lifespanMs, long maxIdleTimeMs) { + boolean replaced = false; + int iteration = 0; + V session = oldVersionEntity.getEntity(); + + while (!replaced && iteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) { + iteration++; + + SessionEntityWrapper newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata()); + + // Atomic cluster-aware replace + replaced = CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache).replace(key, oldVersionEntity, newVersionEntity, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS); + + // Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again + if (!replaced) { + if (LOG.isDebugEnabled()) { + LOG.debugf("Replace failed for entity: %s, old version %s, new version %s. Will try again", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion()); + } + + oldVersionEntity = cache.get(key); + + if (oldVersionEntity == null) { + LOG.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key); + return; + } + + session = oldVersionEntity.getEntity(); + + task.runUpdate(session); + } else { + if (LOG.isTraceEnabled()) { + LOG.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s, Lifespan: %d ms, MaxIdle: %d ms", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion(), task.getLifespanMs(), task.getMaxIdleTimeMs()); + } + } + } + + if (!replaced) { + LOG.warnf("Failed to replace entity '%s' in cache '%s'", key, cache.getName()); + } + + } + + private SessionEntityWrapper generateNewVersionAndWrapEntity(V entity, Map localMetadata) { + return new SessionEntityWrapper<>(localMetadata, entity); + } + + @Override + public void registerChange(Map.Entry> entry, MergedUpdate merged) { + changes.add(() -> runOperationInCluster(entry.getKey(), merged, entry.getValue().getEntityWrapper())); + } + + @Override + public void applyChanges() { + changes.forEach(Runnable::run); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java index 2df4073a2f..c7585abbd8 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java @@ -19,11 +19,13 @@ package org.keycloak.models.sessions.infinispan.changes; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; import org.infinispan.context.Flag; import org.jboss.logging.Logger; +import org.keycloak.common.Profile; import org.keycloak.models.AbstractKeycloakTransaction; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -34,6 +36,9 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; import org.keycloak.connections.infinispan.InfinispanUtil; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; + /** * @author Marek Posolda */ @@ -41,15 +46,15 @@ public class InfinispanChangelogBasedTransaction ext public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class); - private final KeycloakSession kcSession; + protected final KeycloakSession kcSession; private final String cacheName; - private final Cache> cache; + protected final Cache> cache; private final RemoteCacheInvoker remoteCacheInvoker; - private final Map> updates = new HashMap<>(); + protected final Map> updates = new HashMap<>(); - private final SessionFunction lifespanMsLoader; - private final SessionFunction maxIdleTimeMsLoader; + protected final SessionFunction lifespanMsLoader; + protected final SessionFunction maxIdleTimeMsLoader; public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction lifespanMsLoader, SessionFunction maxIdleTimeMsLoader) { @@ -65,6 +70,10 @@ public class InfinispanChangelogBasedTransaction ext public void addTask(K key, SessionUpdateTask task) { SessionUpdatesList myUpdates = updates.get(key); if (myUpdates == null) { + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && (Objects.equals(cacheName, USER_SESSION_CACHE_NAME) || Objects.equals(cacheName, CLIENT_SESSION_CACHE_NAME))) { + throw new IllegalStateException("Can't load from cache"); + } + // Lookup entity from cache SessionEntityWrapper wrappedEntity = cache.get(key); if (wrappedEntity == null) { @@ -95,9 +104,11 @@ public class InfinispanChangelogBasedTransaction ext SessionUpdatesList myUpdates = new SessionUpdatesList<>(realm, wrappedEntity, persistenceState); updates.put(key, myUpdates); - // Run the update now, so reader in same transaction can see it - task.runUpdate(entity); - myUpdates.add(task); + if (task != null) { + // Run the update now, so reader in same transaction can see it + task.runUpdate(entity); + myUpdates.add(task); + } } @@ -150,7 +161,6 @@ public class InfinispanChangelogBasedTransaction ext } } - @Override protected void commitImpl() { for (Map.Entry> entry : updates.entrySet()) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java new file mode 100644 index 0000000000..68cd4f377e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java @@ -0,0 +1,707 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.infinispan.util.function.TriConsumer; +import org.keycloak.common.util.Retry; +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 org.keycloak.models.UserSessionModel; +import org.keycloak.models.delegate.ClientModelLazyDelegate; +import org.keycloak.models.session.PersistentAuthenticatedClientSessionAdapter; +import org.keycloak.models.session.PersistentUserSessionAdapter; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmModelDelegate; +import org.keycloak.models.utils.UserModelDelegate; +import org.keycloak.models.utils.UserSessionModelDelegate; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +public class JpaChangesPerformer implements SessionChangesPerformer { + + private final KeycloakSession session; + private final String cacheName; + private final boolean offline; + private final List> changes = new LinkedList<>(); + private final TriConsumer>, MergedUpdate> processor; + + public JpaChangesPerformer(KeycloakSession session, String cacheName, boolean offline) { + this.session = session; + this.cacheName = cacheName; + this.offline = offline; + processor = processor(); + } + + @Override + public void registerChange(Map.Entry> entry, MergedUpdate merged) { + changes.add(innerSession -> processor.accept(innerSession, entry, merged)); + } + + private TriConsumer>, MergedUpdate> processor() { + return switch (cacheName) { + case "sessions", "offlineSessions" -> this::processUserSessionUpdate; + case "clientSessions", "offlineClientSessions" -> this::processClientSessionUpdate; + default -> throw new IllegalStateException("Unexpected value: " + cacheName); + }; + } + + @Override + public void applyChanges() { + Retry.executeWithBackoff(iteration -> KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), + innerSession -> changes.forEach(c -> c.accept(innerSession))), + 10, 10); + } + + private void processClientSessionUpdate(KeycloakSession innerSession, Map.Entry> entry, MergedUpdate merged) { + SessionUpdatesList sessionUpdates = entry.getValue(); + SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper(); + RealmModel realm = sessionUpdates.getRealm(); + UserSessionPersisterProvider userSessionPersister = innerSession.getProvider(UserSessionPersisterProvider.class); + + if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.REMOVE) { + AuthenticatedClientSessionEntity entity = (AuthenticatedClientSessionEntity) sessionWrapper.getEntity(); + userSessionPersister.removeClientSession(entity.getUserSessionId(), entity.getClientId(), offline); + } else if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD || merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD_IF_ABSENT){ + AuthenticatedClientSessionEntity entity = (AuthenticatedClientSessionEntity) sessionWrapper.getEntity(); + userSessionPersister.createClientSession(new AuthenticatedClientSessionModel() { + @Override + public int getStarted() { + return entity.getStarted(); + } + + @Override + public int getUserSessionStarted() { + return entity.getUserSessionStarted(); + } + + @Override + public boolean isUserSessionRememberMe() { + return entity.isUserSessionRememberMe(); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + throw new IllegalStateException("not implemented"); + } + + @Override + public void detachFromUserSession() { + throw new IllegalStateException("not implemented"); + } + + @Override + public UserSessionModel getUserSession() { + return new UserSessionModelDelegate(null) { + @Override + public String getId() { + return entity.getUserSessionId(); + } + }; + } + + @Override + public String getCurrentRefreshToken() { + return entity.getCurrentRefreshToken(); + } + + @Override + public void setCurrentRefreshToken(String currentRefreshToken) { + throw new IllegalStateException("not implemented"); + } + + @Override + public int getCurrentRefreshTokenUseCount() { + return entity.getCurrentRefreshTokenUseCount(); + } + + @Override + public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { + throw new IllegalStateException("not implemented"); + } + + @Override + public String getNote(String name) { + return entity.getNotes().get(name); + } + + @Override + public void setNote(String name, String value) { + throw new IllegalStateException("not implemented"); + } + + @Override + public void removeNote(String name) { + throw new IllegalStateException("not implemented"); + } + + @Override + public Map getNotes() { + return entity.getNotes(); + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public void setRedirectUri(String uri) { + throw new IllegalStateException("not implemented"); + } + + @Override + public RealmModel getRealm() { + return innerSession.realms().getRealm(entity.getRealmId()); + } + + @Override + public ClientModel getClient() { + return new ClientModelLazyDelegate(() -> null) { + @Override + public String getId() { + return entity.getClientId(); + } + }; + } + + @Override + public String getAction() { + return entity.getAction(); + } + + @Override + public void setAction(String action) { + throw new IllegalStateException("not implemented"); + } + + @Override + public String getProtocol() { + return entity.getAuthMethod(); + } + + @Override + public void setProtocol(String method) { + throw new IllegalStateException("not implemented"); + } + }, offline); + } else { + AuthenticatedClientSessionEntity entity = (AuthenticatedClientSessionEntity) sessionWrapper.getEntity(); + ClientModel client = new ClientModelLazyDelegate(null) { + @Override + public String getId() { + return entity.getClientId(); + } + }; + UserSessionModel userSession = new UserSessionModelDelegate(null) { + @Override + public String getId() { + return entity.getUserSessionId(); + } + }; + PersistentAuthenticatedClientSessionAdapter clientSessionModel = (PersistentAuthenticatedClientSessionAdapter) userSessionPersister.loadClientSession(realm, client, userSession, offline); + if (clientSessionModel != null) { + AuthenticatedClientSessionEntity authenticatedClientSessionEntity = new AuthenticatedClientSessionEntity(entity.getId()) { + @Override + public Map getNotes() { + return new HashMap<>() { + @Override + public String get(Object key) { + return clientSessionModel.getNotes().get(key); + } + + @Override + public String put(String key, String value) { + String oldValue = clientSessionModel.getNotes().get(key); + clientSessionModel.setNote(key, value); + return oldValue; + } + }; + } + + @Override + public void setRedirectUri(String redirectUri) { + clientSessionModel.setRedirectUri(redirectUri); + } + + @Override + public void setTimestamp(int timestamp) { + clientSessionModel.setTimestamp(timestamp); + } + + @Override + public void setCurrentRefreshToken(String currentRefreshToken) { + clientSessionModel.setCurrentRefreshToken(currentRefreshToken); + } + + @Override + public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { + clientSessionModel.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount); + } + + @Override + public void setAction(String action) { + clientSessionModel.setAction(action); + } + + @Override + public void setAuthMethod(String authMethod) { + clientSessionModel.setProtocol(authMethod); + } + + @Override + public String getAuthMethod() { + throw new IllegalStateException("not implemented"); + } + + @Override + public String getRedirectUri() { + return clientSessionModel.getRedirectUri(); + } + + @Override + public int getTimestamp() { + return clientSessionModel.getTimestamp(); + } + + @Override + public int getUserSessionStarted() { + return clientSessionModel.getUserSessionStarted(); + } + + @Override + public int getStarted() { + return clientSessionModel.getStarted(); + } + + @Override + public boolean isUserSessionRememberMe() { + return clientSessionModel.isUserSessionRememberMe(); + } + + @Override + public String getClientId() { + return clientSessionModel.getClient().getClientId(); + } + + @Override + public void setClientId(String clientId) { + throw new IllegalStateException("not implemented"); + } + + @Override + public String getAction() { + return clientSessionModel.getAction(); + } + + @Override + public void setNotes(Map notes) { + clientSessionModel.getNotes().keySet().forEach(clientSessionModel::removeNote); + notes.forEach((k, v) -> clientSessionModel.setNote(k, v)); + } + + @Override + public String getCurrentRefreshToken() { + return clientSessionModel.getCurrentRefreshToken(); + } + + @Override + public int getCurrentRefreshTokenUseCount() { + return clientSessionModel.getCurrentRefreshTokenUseCount(); + } + + @Override + public UUID getId() { + return UUID.fromString(clientSessionModel.getId()); + } + + @Override + public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) { + throw new IllegalStateException("not implemented"); + } + + @Override + public String getUserSessionId() { + return clientSessionModel.getUserSession().getId(); + } + + @Override + public void setUserSessionId(String userSessionId) { + throw new IllegalStateException("not implemented"); + } + }; + sessionUpdates.getUpdateTasks().forEach(vSessionUpdateTask -> { + vSessionUpdateTask.runUpdate((V) authenticatedClientSessionEntity); + if (vSessionUpdateTask.getOperation((V) authenticatedClientSessionEntity) == SessionUpdateTask.CacheOperation.REMOVE) { + userSessionPersister.removeClientSession(entity.getUserSessionId(), entity.getClientId(), offline); + } + }); + clientSessionModel.getUpdatedModel(); + } + } + + } + + private void processUserSessionUpdate(KeycloakSession innerSession, Map.Entry> entry, MergedUpdate merged) { + SessionUpdatesList sessionUpdates = entry.getValue(); + SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper(); + RealmModel realm = sessionUpdates.getRealm(); + UserSessionPersisterProvider userSessionPersister = innerSession.getProvider(UserSessionPersisterProvider.class); + + if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.REMOVE) { + userSessionPersister.removeUserSession(entry.getKey().toString(), offline); + } else if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD || merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD_IF_ABSENT){ + UserSessionEntity entity = (UserSessionEntity) sessionWrapper.getEntity(); + userSessionPersister.createUserSession(new UserSessionModel() { + @Override + public String getId() { + return entity.getId(); + } + + @Override + public RealmModel getRealm() { + return new RealmModelDelegate(null) { + @Override + public String getId() { + return entity.getRealmId(); + } + }; + } + + @Override + public String getBrokerSessionId() { + return entity.getBrokerSessionId(); + } + + @Override + public String getBrokerUserId() { + return entity.getBrokerUserId(); + } + + @Override + public UserModel getUser() { + return new UserModelDelegate(null) { + @Override + public String getId() { + return entity.getUser(); + } + }; + } + + @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) { + throw new IllegalStateException("not implemented"); + } + + @Override + public boolean isOffline() { + return offline; + } + + @Override + public Map getAuthenticatedClientSessions() { + // This is not used when saving this to the database. + return Collections.emptyMap(); + } + + @Override + public void removeAuthenticatedClientSessions(Collection removedClientUUIDS) { + throw new IllegalStateException("not implemented"); + } + + @Override + public String getNote(String name) { + return entity.getNotes().get(name); + } + + @Override + public void setNote(String name, String value) { + throw new IllegalStateException("not implemented"); + } + + @Override + public void removeNote(String name) { + throw new IllegalStateException("not implemented"); + } + + @Override + public Map getNotes() { + return entity.getNotes(); + } + + @Override + public State getState() { + return entity.getState(); + } + + @Override + public void setState(State state) { + throw new IllegalStateException("not implemented"); + } + + @Override + public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + throw new IllegalStateException("not implemented"); + } + }, offline); + } else { + PersistentUserSessionAdapter userSessionModel = (PersistentUserSessionAdapter) userSessionPersister.loadUserSession(realm, entry.getKey().toString(), offline); + if (userSessionModel != null) { + UserSessionEntity userSessionEntity = new UserSessionEntity() { + @Override + public Map getNotes() { + return new HashMap<>() { + + @Override + public String get(Object key) { + return userSessionModel.getNotes().get(key); + } + + @Override + public String put(String key, String value) { + String oldValue = userSessionModel.getNotes().get(key); + userSessionModel.setNote(key, value); + return oldValue; + } + + @Override + public String remove(Object key) { + String oldValue = userSessionModel.getNotes().get(key); + userSessionModel.removeNote(key.toString()); + return oldValue; + } + + @Override + public void clear() { + userSessionModel.getNotes().forEach((k, v) -> userSessionModel.removeNote(k)); + } + }; + } + + @Override + public void setLastSessionRefresh(int lastSessionRefresh) { + userSessionModel.setLastSessionRefresh(lastSessionRefresh); + } + + @Override + public void setState(UserSessionModel.State state) { + userSessionModel.setState(state); + } + + @Override + public AuthenticatedClientSessionStore getAuthenticatedClientSessions() { + return new AuthenticatedClientSessionStore() { + @Override + public void clear() { + userSessionModel.getAuthenticatedClientSessions().clear(); + } + }; + } + + @Override + public String getRealmId() { + return userSessionModel.getRealm().getId(); + } + + @Override + public void setRealmId(String realmId) { + userSessionModel.setRealm(innerSession.realms().getRealm(realmId)); + } + + @Override + public String getId() { + return userSessionModel.getId(); + } + + @Override + public void setId(String id) { + throw new IllegalStateException("not supported"); + } + + @Override + public String getUser() { + return userSessionModel.getUser().getId(); + } + + @Override + public void setUser(String userId) { + userSessionModel.setUser(innerSession.users().getUserById(realm, userId)); + } + + @Override + public String getLoginUsername() { + return userSessionModel.getLoginUsername(); + } + + @Override + public void setLoginUsername(String loginUsername) { + userSessionModel.setLoginUsername(loginUsername); + } + + @Override + public String getIpAddress() { + return userSessionModel.getIpAddress(); + } + + @Override + public void setIpAddress(String ipAddress) { + userSessionModel.setIpAddress(ipAddress); + } + + @Override + public String getAuthMethod() { + return userSessionModel.getAuthMethod(); + } + + @Override + public void setAuthMethod(String authMethod) { + userSessionModel.setAuthMethod(authMethod); + } + + @Override + public boolean isRememberMe() { + return userSessionModel.isRememberMe(); + } + + @Override + public void setRememberMe(boolean rememberMe) { + userSessionModel.setRememberMe(rememberMe); + } + + @Override + public int getStarted() { + return userSessionModel.getStarted(); + } + + @Override + public void setStarted(int started) { + userSessionModel.setStarted(started); + } + + @Override + public int getLastSessionRefresh() { + return userSessionModel.getLastSessionRefresh(); + } + + @Override + public void setNotes(Map notes) { + userSessionModel.getNotes().keySet().forEach(userSessionModel::removeNote); + notes.forEach((k, v) -> userSessionModel.setNote(k, v)); + } + + @Override + public void setAuthenticatedClientSessions(AuthenticatedClientSessionStore authenticatedClientSessions) { + throw new IllegalStateException("not supported"); + } + + @Override + public UserSessionModel.State getState() { + return userSessionModel.getState(); + } + + @Override + public String getBrokerSessionId() { + return userSessionModel.getBrokerSessionId(); + } + + @Override + public void setBrokerSessionId(String brokerSessionId) { + userSessionModel.setBrokerSessionId(brokerSessionId); + } + + @Override + public String getBrokerUserId() { + return userSessionModel.getBrokerUserId(); + } + + @Override + public void setBrokerUserId(String brokerUserId) { + userSessionModel.setBrokerUserId(brokerUserId); + } + + @Override + public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) { + throw new IllegalStateException("not supported"); + } + }; + sessionUpdates.getUpdateTasks().forEach(vSessionUpdateTask -> { + vSessionUpdateTask.runUpdate((V) userSessionEntity); + if (vSessionUpdateTask.getOperation((V)userSessionEntity) == SessionUpdateTask.CacheOperation.REMOVE) { + userSessionPersister.removeUserSession(entry.getKey().toString(), offline); + } + }); + userSessionModel.getUpdatedModel(); + } + + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/PersistentSessionsChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/PersistentSessionsChangelogBasedTransaction.java new file mode 100644 index 0000000000..fbec388528 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/PersistentSessionsChangelogBasedTransaction.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.infinispan.Cache; +import org.keycloak.common.Profile; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.SessionFunction; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; + +import java.util.List; +import java.util.Map; + +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; + +public class PersistentSessionsChangelogBasedTransaction extends InfinispanChangelogBasedTransaction { + + private final List> changesPerformers; + protected final boolean offline; + + public PersistentSessionsChangelogBasedTransaction(KeycloakSession session, Cache> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction lifespanMsLoader, SessionFunction maxIdleTimeMsLoader, boolean offline) { + super(session, cache, remoteCacheInvoker, lifespanMsLoader, maxIdleTimeMsLoader); + this.offline = offline; + + if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + throw new IllegalStateException("Persistent user sessions are not enabled"); + } + + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && (cache.getName().equals(USER_SESSION_CACHE_NAME) || cache.getName().equals(CLIENT_SESSION_CACHE_NAME))) { + changesPerformers = List.of( + new JpaChangesPerformer<>(session, cache.getName(), offline) + ); + } else { + changesPerformers = List.of( + new JpaChangesPerformer<>(session, cache.getName(), offline), + new EmbeddedCachesChangesPerformer<>(cache), + new RemoteCachesChangesPerformer<>(session, cache, remoteCacheInvoker) + ); + } + } + + @Override + protected void commitImpl() { + for (Map.Entry> entry : updates.entrySet()) { + SessionUpdatesList sessionUpdates = entry.getValue(); + SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper(); + + // Don't save transient entities to infinispan. They are valid just for current transaction + if (sessionUpdates.getPersistenceState() == UserSessionModel.SessionPersistenceState.TRANSIENT) continue; + + RealmModel realm = sessionUpdates.getRealm(); + + long lifespanMs = lifespanMsLoader.apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity()); + long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity()); + + MergedUpdate merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, lifespanMs, maxIdleTimeMs); + + if (merged != null) { + changesPerformers.forEach(p -> p.registerChange(entry, merged)); + } + } + + changesPerformers.forEach(SessionChangesPerformer::applyChanges); + } + + @Override + protected void rollbackImpl() { + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/RemoteCachesChangesPerformer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/RemoteCachesChangesPerformer.java new file mode 100644 index 0000000000..d298815526 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/RemoteCachesChangesPerformer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.infinispan.Cache; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class RemoteCachesChangesPerformer implements SessionChangesPerformer { + + private final KeycloakSession session; + private final Cache> cache; + private final RemoteCacheInvoker remoteCacheInvoker; + private final List changes = new LinkedList<>(); + + + public RemoteCachesChangesPerformer(KeycloakSession session, Cache> cache, RemoteCacheInvoker remoteCacheInvoker) { + this.session = session; + this.cache = cache; + this.remoteCacheInvoker = remoteCacheInvoker; + } + + @Override + public void registerChange(Map.Entry> entry, MergedUpdate merged) { + SessionUpdatesList updates = entry.getValue(); + changes.add(() -> remoteCacheInvoker.runTask(session, updates.getRealm(), cache.getName(), entry.getKey(), merged, updates.getEntityWrapper())); + } + + @Override + public void applyChanges() { + changes.forEach(Runnable::run); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionChangesPerformer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionChangesPerformer.java new file mode 100644 index 0000000000..51640c5d14 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionChangesPerformer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.util.Map; + +public interface SessionChangesPerformer { + void registerChange(Map.Entry> entry, MergedUpdate merged); + + void applyChanges(); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java new file mode 100644 index 0000000000..adc8fa2b6c --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import org.infinispan.Cache; +import org.jboss.logging.Logger; +import org.keycloak.common.Profile; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider; +import org.keycloak.models.sessions.infinispan.SessionFunction; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; + +import java.util.Collections; +import java.util.Objects; + +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; + +public class UserSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction { + + private static final Logger LOG = Logger.getLogger(UserSessionPersistentChangelogBasedTransaction.class); + public UserSessionPersistentChangelogBasedTransaction(KeycloakSession session, Cache> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction lifespanMsLoader, SessionFunction maxIdleTimeMsLoader, boolean offline) { + super(session, cache, remoteCacheInvoker, lifespanMsLoader, maxIdleTimeMsLoader, offline); + } + + public SessionEntityWrapper get(RealmModel realm, String key) { + SessionUpdatesList myUpdates = updates.get(key); + if (myUpdates == null) { + SessionEntityWrapper wrappedEntity = null; + if (!((Objects.equals(cache.getName(), USER_SESSION_CACHE_NAME) || Objects.equals(cache.getName(), CLIENT_SESSION_CACHE_NAME)) && Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE))) { + wrappedEntity = cache.get(key); + } + if (wrappedEntity == null) { + wrappedEntity = getSessionEntityFromPersister(realm, key); + } + + if (wrappedEntity == null) { + return null; + } + + RealmModel realmFromSession = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId()); + if (!realmFromSession.getId().equals(realm.getId())) { + LOG.warnf("Realm mismatch for session %s. Expected realm %s, but found realm %s", wrappedEntity.getEntity(), realm.getId(), realmFromSession.getId()); + return null; + } + + myUpdates = new SessionUpdatesList<>(realm, wrappedEntity); + updates.put(key, myUpdates); + + return wrappedEntity; + } else { + UserSessionEntity entity = myUpdates.getEntityWrapper().getEntity(); + + // If entity is scheduled for remove, we don't return it. + boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> { + + return task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE; + + }).findFirst().isPresent(); + + return scheduledForRemove ? null : myUpdates.getEntityWrapper(); + } + } + + public SessionEntityWrapper getSessionEntityFromPersister(RealmModel realm, String key) { + UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class); + UserSessionModel persistentUserSession = persister.loadUserSession(realm, key, offline); + + if (persistentUserSession == null) { + return null; + } + + SessionEntityWrapper userSessionEntitySessionEntityWrapper = importUserSession(persistentUserSession); + if (userSessionEntitySessionEntityWrapper == null) { + removeSessionEntityFromPersister(key); + } + + return userSessionEntitySessionEntityWrapper; + } + + private void removeSessionEntityFromPersister(String key) { + UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class); + persister.removeUserSession(key, offline); + } + + private SessionEntityWrapper importUserSession(UserSessionModel persistentUserSession) { + String sessionId = persistentUserSession.getId(); + + if (isScheduledForRemove(sessionId)) { + return null; + } + + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && (cache.getName().equals(USER_SESSION_CACHE_NAME) || cache.getName().equals(CLIENT_SESSION_CACHE_NAME))) { + return ((PersistentUserSessionProvider) kcSession.getProvider(UserSessionProvider.class)).wrapPersistentEntity(persistentUserSession.getRealm(), offline, persistentUserSession); + } + + LOG.debugf("Attempting to import user-session for sessionId=%s offline=%s", sessionId, offline); + kcSession.sessions().importUserSessions(Collections.singleton(persistentUserSession), offline); + LOG.debugf("user-session imported, trying another lookup for sessionId=%s offline=%s", sessionId, offline); + + SessionEntityWrapper ispnUserSessionEntity = cache.get(sessionId); + + if (ispnUserSessionEntity != null) { + LOG.debugf("user-session found after import for sessionId=%s offline=%s", sessionId, offline); + return ispnUserSessionEntity; + } + + LOG.debugf("user-session could not be found after import for sessionId=%s offline=%s", sessionId, offline); + return null; + } + public boolean isScheduledForRemove(String key) { + return isScheduledForRemove(updates.get(key)); + } + + private static boolean isScheduledForRemove(SessionUpdatesList myUpdates) { + if (myUpdates == null) { + return false; + } + + V entity = myUpdates.getEntityWrapper().getEntity(); + + // If entity is scheduled for remove, we don't return it. + boolean scheduledForRemove = myUpdates.getUpdateTasks() + .stream() + .anyMatch(task -> task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE); + + return scheduledForRemove; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java index 3f49f3b985..d3fcba7c10 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java @@ -57,6 +57,8 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { private final UUID id; + private transient String userSessionId; + public AuthenticatedClientSessionEntity(UUID id) { this.id = id; } @@ -190,6 +192,14 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { return entityWrapper; } + public String getUserSessionId() { + return userSessionId; + } + + public void setUserSessionId(String userSessionId) { + this.userSessionId = userSessionId; + } + public static class ExternalizerImpl implements Externalizer { @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java index e0e7be264b..c87b125f08 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java @@ -17,6 +17,7 @@ package org.keycloak.models.sessions.infinispan.stream; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.infinispan.AuthenticatedClientSessionAdapter; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; @@ -165,6 +166,51 @@ public class UserSessionPredicate implements Predicate toModelPredicate() { + + return (Predicate) entity -> { + if (!realm.equals(entity.getRealm().getId())) { + return false; + } + + if (user != null && !entity.getUser().getId().equals(user)) { + return false; + } + + if (client != null && (entity.getAuthenticatedClientSessions() == null || !entity.getAuthenticatedClientSessions().containsKey(client))) { + return false; + } + + if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) { + return false; + } + + if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) { + return false; + } + + if (entity.isRememberMe()) { + if (expiredRememberMe != null && expiredRefreshRememberMe != null && entity.getStarted() > expiredRememberMe && entity.getLastSessionRefresh() > expiredRefreshRememberMe) { + return false; + } + } else { + if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) { + return false; + } + } + + if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) { + return false; + } + return true; + }; + } + + public static class ExternalizerImpl implements Externalizer { private static final int VERSION_1 = 1; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java index 3bdfd6f76d..986b4bc633 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -18,10 +18,12 @@ package org.keycloak.models.jpa.session; import org.jboss.logging.Logger; +import org.keycloak.common.Profile; 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.OfflineUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -38,7 +40,6 @@ import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -81,6 +82,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv entity.setOffline(offlineStr); entity.setLastSessionRefresh(model.getLastSessionRefresh()); entity.setData(model.getData()); + entity.setBrokerSessionId(userSession.getBrokerSessionId()); em.persist(entity); em.flush(); } @@ -165,7 +167,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv // Remove userSession if it was last clientSession List clientSessions = getClientSessionsByUserSession(sessionEntity.getUserSessionId(), offline); - if (clientSessions.size() == 0) { + if (clientSessions.size() == 0 && offline) { offlineStr = offlineToString(offline); PersistentUserSessionEntity userSessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(sessionEntity.getUserSessionId(), offlineStr), LockModeType.PESSIMISTIC_WRITE); if (userSessionEntity != null) { @@ -252,7 +254,24 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv expiredClientOffline = Time.currentTime() - realm.getClientOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS; } - String offlineStr = offlineToString(true); + expire(realm, expiredClientOffline, expiredOffline, true); + + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + + int expired = Time.currentTime() - Math.max(realm.getSsoSessionIdleTimeout(), realm.getSsoSessionIdleTimeoutRememberMe()) - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS; + + // prefer client session timeout if set + int expiredClient = expired; + if (realm.getClientSessionIdleTimeout() > 0) { + expiredClient = Time.currentTime() - realm.getClientSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS; + } + + expire(realm, expiredClient, expired, false); + } + } + + private void expire(RealmModel realm, int expiredClientOffline, int expiredOffline, boolean offline) { + String offlineStr = offlineToString(offline); logger.tracef("Trigger removing expired user sessions for realm '%s'", realm.getName()); @@ -269,7 +288,6 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv .executeUpdate(); logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName()); - } @Override @@ -305,7 +323,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv userSessionQuery.setParameter("userSessionId", userSessionId); userSessionQuery.setMaxResults(1); - Stream persistentUserSessions = closing(userSessionQuery.getResultStream().map(this::toAdapter)); + Stream persistentUserSessions = closing(userSessionQuery.getResultStream().map(this::toAdapter)); return persistentUserSessions.findAny().map(userSession -> { @@ -330,6 +348,41 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv }).orElse(null); } + @Override + public UserSessionModel loadUserSessionsStreamByBrokerSessionId(RealmModel realm, String brokerSessionId, boolean offline) { + + TypedQuery userSessionQuery = em.createNamedQuery("findUserSessionsByBrokerSessionId", PersistentUserSessionEntity.class); + userSessionQuery.setParameter("realmId", realm.getId()); + userSessionQuery.setParameter("brokerSessionId", brokerSessionId); + userSessionQuery.setParameter("offline", offlineToString(offline)); + userSessionQuery.setMaxResults(1); + + Stream persistentUserSessions = closing(userSessionQuery.getResultStream().map(this::toAdapter)); + + return persistentUserSessions.findAny().map(userSession -> { + + TypedQuery clientSessionQuery = em.createNamedQuery("findClientSessionsByUserSession", PersistentClientSessionEntity.class); + clientSessionQuery.setParameter("userSessionId", userSession.getId()); + clientSessionQuery.setParameter("offline", offlineToString(userSession.isOffline())); + + Set removedClientUUIDs = new HashSet<>(); + + closing(clientSessionQuery.getResultStream()).forEach(clientSession -> { + boolean added = addClientSessionToAuthenticatedClientSessionsIfPresent(userSession, clientSession); + if (!added) { + // client was removed in the meantime + removedClientUUIDs.add(clientSession.getClientId()); + } + } + ); + + removedClientUUIDs.forEach(this::onClientRemoved); + + return userSession; + }).orElse(null); + } + + @Override public Stream loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults) { @@ -417,12 +470,12 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv * @return */ private Stream loadUserSessionsWithClientSessions(TypedQuery query, String offlineStr, boolean useExact) { - List userSessionAdapters = closing(query.getResultStream() + List userSessionAdapters = closing(query.getResultStream() .map(this::toAdapter) .filter(Objects::nonNull)) .collect(Collectors.toList()); - Map sessionsById = userSessionAdapters.stream() + Map sessionsById = userSessionAdapters.stream() .collect(Collectors.toMap(UserSessionModel::getId, Function.identity())); Set userSessionIds = sessionsById.keySet(); @@ -446,7 +499,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv } closing(queryClientSessions.getResultStream()).forEach(clientSession -> { - PersistentUserSessionAdapter userSession = sessionsById.get(clientSession.getUserSessionId()); + OfflineUserSessionModel userSession = sessionsById.get(clientSession.getUserSessionId()); // check if we have a user session for the client session if (userSession != null) { boolean added = addClientSessionToAuthenticatedClientSessionsIfPresent(userSession, clientSession); @@ -467,9 +520,9 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv return userSessionAdapters.stream().map(UserSessionModel.class::cast); } - private boolean addClientSessionToAuthenticatedClientSessionsIfPresent(PersistentUserSessionAdapter userSession, PersistentClientSessionEntity clientSessionEntity) { + private boolean addClientSessionToAuthenticatedClientSessionsIfPresent(OfflineUserSessionModel userSession, PersistentClientSessionEntity clientSessionEntity) { - PersistentAuthenticatedClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSessionEntity); + AuthenticatedClientSessionModel clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSessionEntity); if (clientSessAdapter.getClient() == null) { logger.tracef("Not adding client session %s / %s since client is null", userSession, clientSessAdapter); @@ -487,7 +540,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv return true; } - private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) { + private OfflineUserSessionModel toAdapter(PersistentUserSessionEntity entity) { RealmModel realm = session.realms().getRealm(entity.getRealmId()); if (realm == null) { // Realm has been deleted concurrently, ignore the entity return null; @@ -495,19 +548,79 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv return toAdapter(realm, entity); } - private PersistentUserSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionEntity entity) { - PersistentUserSessionModel model = new PersistentUserSessionModel(); - model.setUserSessionId(entity.getUserSessionId()); - model.setStarted(entity.getCreatedOn()); - model.setLastSessionRefresh(entity.getLastSessionRefresh()); - model.setData(entity.getData()); - model.setOffline(offlineFromString(entity.getOffline())); + private OfflineUserSessionModel toAdapter(RealmModel realm, PersistentUserSessionEntity entity) { + PersistentUserSessionModel model = new PersistentUserSessionModel() { + @Override + public String getUserSessionId() { + return entity.getUserSessionId(); + } + + @Override + public void setUserSessionId(String userSessionId) { + entity.setUserSessionId(userSessionId); + } + + @Override + public int getStarted() { + return entity.getCreatedOn(); + } + + @Override + public void setStarted(int started) { + entity.setCreatedOn(started); + } + + @Override + public int getLastSessionRefresh() { + return entity.getLastSessionRefresh(); + } + + @Override + public void setLastSessionRefresh(int lastSessionRefresh) { + entity.setLastSessionRefresh(lastSessionRefresh); + } + + @Override + public boolean isOffline() { + return offlineFromString(entity.getOffline()); + } + + @Override + public void setOffline(boolean offline) { + entity.setOffline(offlineToString(offline)); + } + + @Override + public String getData() { + return entity.getData(); + } + + @Override + public void setData(String data) { + entity.setData(data); + } + + @Override + public void setRealmId(String realmId) { + entity.setRealmId(realmId); + } + + @Override + public void setUserId(String userId) { + entity.setUserId(userId); + } + + @Override + public void setBrokerSessionId(String brokerSessionId) { + entity.setBrokerSessionId(brokerSessionId); + } + }; Map clientSessions = new HashMap<>(); return new PersistentUserSessionAdapter(session, model, realm, entity.getUserId(), clientSessions); } - private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, UserSessionModel userSession, PersistentClientSessionEntity entity) { + private AuthenticatedClientSessionModel toAdapter(RealmModel realm, UserSessionModel userSession, PersistentClientSessionEntity entity) { String clientId = entity.getClientId(); if (isExternalClient(entity)) { clientId = getExternalClientId(entity); @@ -515,21 +628,51 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv // can be null if client is not found anymore ClientModel client = realm.getClientById(clientId); - PersistentClientSessionModel model = new PersistentClientSessionModel(); - model.setClientId(clientId); - model.setUserSessionId(userSession.getId()); - - if (userSession instanceof PersistentUserSessionAdapter) { - model.setUserId(((PersistentUserSessionAdapter) userSession).getUserId()); - } - else { - UserModel user = userSession.getUser(); - if (user != null) { - model.setUserId(user.getId()); + PersistentClientSessionModel model = new PersistentClientSessionModel() { + @Override + public String getUserSessionId() { + return entity.getUserSessionId(); } - } - model.setTimestamp(entity.getTimestamp()); - model.setData(entity.getData()); + + @Override + public void setUserSessionId(String userSessionId) { + entity.setUserSessionId(userSessionId); + } + + @Override + public String getClientId() { + String clientId = entity.getClientId(); + if (isExternalClient(entity)) { + clientId = getExternalClientId(entity); + } + return clientId; + } + + @Override + public void setClientId(String clientId) { + throw new IllegalStateException("forbidden"); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + } + + @Override + public String getData() { + return entity.getData(); + } + + @Override + public void setData(String data) { + entity.setData(data); + } + }; return new PersistentAuthenticatedClientSessionAdapter(session, model, realm, client, userSession); } @@ -565,6 +708,19 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv return n.intValue(); } + @Override + public void removeUserSessions(RealmModel realm, boolean offline) { + em.createNamedQuery("deleteClientSessionsByRealmSessionType") + .setParameter("realmId", realm.getId()) + .setParameter("offline", offlineToString(offline)) + .executeUpdate(); + em.createNamedQuery("deleteUserSessionsByRealmSessionType") + .setParameter("realmId", realm.getId()) + .setParameter("offline", offlineToString(offline)) + .executeUpdate(); + } + + @Override public void close() { // NOOP diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java index 8524893684..832197d4f9 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -24,6 +24,8 @@ import jakarta.persistence.IdClass; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; import jakarta.persistence.Table; +import jakarta.persistence.Version; + import java.io.Serializable; /** @@ -77,6 +79,9 @@ public class PersistentClientSessionEntity { @Column(name="TIMESTAMP") protected int timestamp; + @Version + private int version; + @Id @Column(name = "OFFLINE_FLAG") protected String offline; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java index c711a47dc4..78198658a9 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java @@ -17,6 +17,7 @@ package org.keycloak.models.jpa.session; +import jakarta.persistence.Version; import org.keycloak.storage.jpa.KeyUtils; import jakarta.persistence.Column; @@ -33,6 +34,7 @@ import java.io.Serializable; */ @NamedQueries({ @NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId"), + @NamedQuery(name="deleteUserSessionsByRealmSessionType", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId and sess.offline = :offline"), @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId = :userId"), @NamedQuery(name="deleteExpiredUserSessions", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId AND sess.offline = :offline AND sess.lastSessionRefresh < :lastSessionRefresh"), @NamedQuery(name="updateUserSessionLastSessionRefresh", query="update PersistentUserSessionEntity sess set lastSessionRefresh = :lastSessionRefresh where sess.realmId = :realmId" + @@ -45,14 +47,16 @@ import java.io.Serializable; " AND sess.userSessionId = :userSessionId AND sess.realmId = :realmId"), @NamedQuery(name="findUserSessionsByUserId", query="select sess from PersistentUserSessionEntity sess where sess.offline = :offline" + " AND sess.realmId = :realmId AND sess.userId = :userId ORDER BY sess.userSessionId"), + @NamedQuery(name="findUserSessionsByBrokerSessionId", query="select sess from PersistentUserSessionEntity sess where sess.brokerSessionId = :brokerSessionId" + + " AND sess.realmId = :realmId AND sess.offline = :offline ORDER BY sess.userSessionId"), @NamedQuery(name="findUserSessionsByClientId", query="SELECT sess FROM PersistentUserSessionEntity sess INNER JOIN PersistentClientSessionEntity clientSess " + - " ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientId = :clientId WHERE sess.offline = :offline " + + " ON sess.userSessionId = clientSess.userSessionId AND sess.offline = clientSess.offline AND clientSess.clientId = :clientId WHERE sess.offline = :offline " + " AND sess.realmId = :realmId ORDER BY sess.userSessionId"), @NamedQuery(name="findUserSessionsByExternalClientId", query="SELECT sess FROM PersistentUserSessionEntity sess INNER JOIN PersistentClientSessionEntity clientSess " + - " ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientStorageProvider = :clientStorageProvider AND clientSess.externalClientId = :externalClientId WHERE sess.offline = :offline " + + " ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientStorageProvider = :clientStorageProvider AND sess.offline = clientSess.offline AND clientSess.externalClientId = :externalClientId WHERE sess.offline = :offline " + " AND sess.realmId = :realmId ORDER BY sess.userSessionId"), @NamedQuery(name="findClientSessionsClientIds", query="SELECT clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider, count(clientSess)" + - " FROM PersistentClientSessionEntity clientSess INNER JOIN PersistentUserSessionEntity sess ON clientSess.userSessionId = sess.userSessionId " + + " FROM PersistentClientSessionEntity clientSess INNER JOIN PersistentUserSessionEntity sess ON clientSess.userSessionId = sess.userSessionId AND sess.offline = clientSess.offline" + " WHERE sess.offline = :offline AND sess.realmId = :realmId " + " GROUP BY clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider") @@ -78,6 +82,12 @@ public class PersistentUserSessionEntity { @Column(name = "LAST_SESSION_REFRESH") protected int lastSessionRefresh; + @Column(name = "BROKER_SESSION_ID") + protected String brokerSessionId; + + @Version + private int version; + @Id @Column(name = "OFFLINE_FLAG") protected String offline; @@ -134,6 +144,14 @@ public class PersistentUserSessionEntity { this.offline = offline; } + public String getBrokerSessionId() { + return brokerSessionId; + } + + public void setBrokerSessionId(String brokerSessionId) { + this.brokerSessionId = brokerSessionId; + } + public String getData() { return data; } diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java index ac2bc2584a..f86f9fb38d 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java @@ -18,6 +18,7 @@ package org.keycloak.storage.jpa; import java.util.regex.Pattern; import org.jboss.logging.Logger; +import org.keycloak.models.light.LightweightUserAdapter; /** * @@ -33,6 +34,8 @@ public class KeyUtils { UUID_PATTERN.pattern() + "|" + "f:" + UUID_PATTERN.pattern() + ":.*" + + "|" + + LightweightUserAdapter.ID_PREFIX + UUID_PATTERN.pattern() ); /** diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml index 2780bffeb6..816cb1c1ba 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml @@ -17,6 +17,63 @@ --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/queries-default.properties b/model/jpa/src/main/resources/META-INF/queries-default.properties index 1759216dac..749eb034cb 100644 --- a/model/jpa/src/main/resources/META-INF/queries-default.properties +++ b/model/jpa/src/main/resources/META-INF/queries-default.properties @@ -5,7 +5,7 @@ # type can be native (for native queries) or jpql (jpql syntax) # if no type is defined jpql is the default -deleteExpiredClientSessions=delete from PersistentClientSessionEntity sess where sess.userSessionId IN (\ +deleteExpiredClientSessions=delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (\ select u.userSessionId from PersistentUserSessionEntity u \ where u.realmId = :realmId AND u.offline = :offline AND u.lastSessionRefresh < :lastSessionRefresh) @@ -13,6 +13,10 @@ deleteClientSessionsByRealm=delete from PersistentClientSessionEntity sess where select u.userSessionId from PersistentUserSessionEntity u \ where u.realmId = :realmId) +deleteClientSessionsByRealmSessionType=delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (\ + select u.userSessionId from PersistentUserSessionEntity u \ + where u.realmId = :realmId and u.offline = :offline) + deleteClientSessionsByUser=delete from PersistentClientSessionEntity sess where sess.userSessionId IN (\ select u.userSessionId from PersistentUserSessionEntity u \ where u.userId = :userId) diff --git a/model/jpa/src/main/resources/META-INF/queries-mariadb.properties b/model/jpa/src/main/resources/META-INF/queries-mariadb.properties index cb3c69fdb1..b2422319da 100644 --- a/model/jpa/src/main/resources/META-INF/queries-mariadb.properties +++ b/model/jpa/src/main/resources/META-INF/queries-mariadb.properties @@ -5,11 +5,14 @@ # if no type is defined jpql is the default deleteExpiredClientSessions[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ - where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline \ + where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = c.OFFLINE_FLAG and u.OFFLINE_FLAG = :offline \ and u.LAST_SESSION_REFRESH < :lastSessionRefresh deleteClientSessionsByRealm[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId +deleteClientSessionsByRealmSessionType[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ + where c.USER_SESSION_ID = u.USER_SESSION_ID and u.OFFLINE_FLAG = c.OFFLINE_FLAG AND u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline + deleteClientSessionsByUser[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ where c.USER_SESSION_ID = u.USER_SESSION_ID and u.USER_ID = :userId diff --git a/model/jpa/src/main/resources/META-INF/queries-mysql.properties b/model/jpa/src/main/resources/META-INF/queries-mysql.properties index cb3c69fdb1..b2422319da 100644 --- a/model/jpa/src/main/resources/META-INF/queries-mysql.properties +++ b/model/jpa/src/main/resources/META-INF/queries-mysql.properties @@ -5,11 +5,14 @@ # if no type is defined jpql is the default deleteExpiredClientSessions[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ - where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline \ + where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = c.OFFLINE_FLAG and u.OFFLINE_FLAG = :offline \ and u.LAST_SESSION_REFRESH < :lastSessionRefresh deleteClientSessionsByRealm[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId +deleteClientSessionsByRealmSessionType[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ + where c.USER_SESSION_ID = u.USER_SESSION_ID and u.OFFLINE_FLAG = c.OFFLINE_FLAG AND u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline + deleteClientSessionsByUser[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \ where c.USER_SESSION_ID = u.USER_SESSION_ID and u.USER_ID = :userId diff --git a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java index 00a7155127..791c68383d 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java +++ b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java @@ -52,9 +52,45 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate data.setNotes(clientSession.getNotes()); data.setRedirectUri(clientSession.getRedirectUri()); - model = new PersistentClientSessionModel(); + model = new PersistentClientSessionModel() { + private String userSessionId; + private String clientId; + private int timestamp; + private String data; + + public String getUserSessionId() { + return userSessionId; + } + + public void setUserSessionId(String userSessionId) { + this.userSessionId = userSessionId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + }; model.setClientId(clientSession.getClient().getId()); - model.setUserId(clientSession.getUserSession().getUser().getId()); model.setUserSessionId(clientSession.getUserSession().getId()); model.setTimestamp(clientSession.getTimestamp()); @@ -151,22 +187,22 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate @Override public String getCurrentRefreshToken() { - return null; // Information not persisted. + return getData().getCurrentRefreshToken(); } @Override public void setCurrentRefreshToken(String currentRefreshToken) { - // Information not persisted. + getData().setCurrentRefreshToken(currentRefreshToken); } @Override public int getCurrentRefreshTokenUseCount() { - return 0; // Information not persisted. + return getData().getCurrentRefreshTokenUseCount(); } @Override public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { - // Information not persisted. + getData().setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount); } @Override @@ -263,7 +299,10 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate private Set protocolMappers; @JsonProperty("roles") private Set roles; - + @JsonProperty("currentRefreshToken") + private String currentRefreshToken; + @JsonProperty("currentRefreshTokenUseCount") + private int currentRefreshTokenUseCount; public String getAuthMethod() { return authMethod; @@ -336,5 +375,21 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate public void setRoles(Set roles) { this.roles = roles; } + + public String getCurrentRefreshToken() { + return currentRefreshToken; + } + + public void setCurrentRefreshToken(String currentRefreshToken) { + this.currentRefreshToken = currentRefreshToken; + } + + public int getCurrentRefreshTokenUseCount() { + return currentRefreshTokenUseCount; + } + + public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) { + this.currentRefreshTokenUseCount = currentRefreshTokenUseCount; + } } } diff --git a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java index ee33fedf03..648ebb9cfd 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java +++ b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java @@ -20,52 +20,21 @@ package org.keycloak.models.session; /** * @author Marek Posolda */ -public class PersistentClientSessionModel { +public interface PersistentClientSessionModel { - private String userSessionId; - private String clientId; - private String userId; - private int timestamp; - private String data; + String getUserSessionId(); + void setUserSessionId(String userSessionId); - public String getUserSessionId() { - return userSessionId; - } + String getClientId(); - public void setUserSessionId(String userSessionId) { - this.userSessionId = userSessionId; - } + void setClientId(String clientId); - public String getClientId() { - return clientId; - } + int getTimestamp(); - public void setClientId(String clientId) { - this.clientId = clientId; - } + void setTimestamp(int timestamp); - public String getUserId() { - return userId; - } + String getData(); - public void setUserId(String userId) { - this.userId = userId; - } - - public int getTimestamp() { - return timestamp; - } - - public void setTimestamp(int timestamp) { - this.timestamp = timestamp; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } + void setData(String data); } diff --git a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index da5cb96bd6..2655a5932e 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java +++ b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -18,6 +18,7 @@ package org.keycloak.models.session; import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.common.Profile; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; @@ -25,6 +26,7 @@ import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.util.JsonSerialization; import java.io.IOException; @@ -32,6 +34,8 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER; + /** * @author Marek Posolda */ @@ -40,7 +44,7 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { private final PersistentUserSessionModel model; private UserModel user; private String userId; - private final RealmModel realm; + private RealmModel realm; private KeycloakSession session; private final Map authenticatedClientSessions; @@ -58,7 +62,78 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { data.setState(other.getState().toString()); } - this.model = new PersistentUserSessionModel(); + this.model = new PersistentUserSessionModel() { + private String userSessionId; + private int started; + private int lastSessionRefresh; + private boolean offline; + private String data; + + @Override + public String getUserSessionId() { + return userSessionId; + } + + @Override + public void setUserSessionId(String userSessionId) { + this.userSessionId = userSessionId; + } + + @Override + public int getStarted() { + return started; + } + + @Override + public void setStarted(int started) { + this.started = started; + } + + @Override + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + @Override + public void setLastSessionRefresh(int lastSessionRefresh) { + this.lastSessionRefresh = lastSessionRefresh; + } + + @Override + public boolean isOffline() { + return offline; + } + + @Override + public void setOffline(boolean offline) { + this.offline = offline; + } + + @Override + public String getData() { + return data; + } + + @Override + public void setData(String data) { + this.data = data; + } + + @Override + public void setRealmId(String realmId) { + /* ignored */ + } + + @Override + public void setUserId(String userId) { + /* ignored */ + } + + @Override + public void setBrokerSessionId(String brokerSessionId) { + /* ignored */ + } + }; this.model.setStarted(other.getStarted()); this.model.setUserSessionId(other.getId()); this.model.setLastSessionRefresh(other.getLastSessionRefresh()); @@ -120,7 +195,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { @Override public UserModel getUser() { if (user == null) { - user = session.users().getUserById(realm, userId); + if (LightweightUserAdapter.isLightweightUser(userId)) { + user = LightweightUserAdapter.fromString(session, realm, getData().getNotes().get(SESSION_NOTE_LIGHTWEIGHT_USER)); + } else { + user = session.users().getUserById(realm, userId); + } } return user; } @@ -137,7 +216,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { @Override public String getLoginUsername() { - return getUser().getUsername(); + if (isOffline() || !Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + return getUser().getUsername(); + } else { + return getData().getLoginUsername(); + } } @Override @@ -243,6 +326,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { throw new IllegalStateException("Not supported"); } + @Override + public void setLoginUsername(String loginUsername) { + getData().setLoginUsername(loginUsername); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -262,6 +350,41 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { return getId(); } + public void setRealm(RealmModel realm) { + this.realm = realm; + model.setRealmId(realm.getId()); + } + + public void setUser(UserModel user) { + this.user = user; + model.setUserId(user.getId()); + } + + public void setIpAddress(String ipAddress) { + getData().setIpAddress(ipAddress); + } + + public void setAuthMethod(String authMethod) { + getData().setAuthMethod(authMethod); + } + + public void setRememberMe(boolean rememberMe) { + getData().setRememberMe(rememberMe); + } + + public void setStarted(int started) { + getData().setStarted(started); + } + + public void setBrokerSessionId(String brokerSessionId) { + getData().setBrokerSessionId(brokerSessionId); + model.setBrokerSessionId(brokerSessionId); + } + + public void setBrokerUserId(String brokerUserId) { + getData().setBrokerUserId(brokerUserId); + } + protected static class PersistentUserSessionData { @JsonProperty("brokerSessionId") @@ -289,6 +412,9 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { @JsonProperty("state") private String state; + @JsonProperty("loginUsername") + private String loginUsername; + public String getBrokerSessionId() { return brokerSessionId; } @@ -354,5 +480,13 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { public void setState(String state) { this.state = state; } + + public void setLoginUsername(String loginUsername) { + this.loginUsername = loginUsername; + } + + public String getLoginUsername() { + return loginUsername; + } } } diff --git a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java index 508b81741b..cd70b2a4d7 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java +++ b/model/storage-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java @@ -20,51 +20,32 @@ package org.keycloak.models.session; /** * @author Marek Posolda */ -public class PersistentUserSessionModel { +public interface PersistentUserSessionModel { - private String userSessionId; - private int started; - private int lastSessionRefresh; - private boolean offline; - private String data; + String getUserSessionId(); - public String getUserSessionId() { - return userSessionId; - } + void setUserSessionId(String userSessionId); - public void setUserSessionId(String userSessionId) { - this.userSessionId = userSessionId; - } + int getStarted(); - public int getStarted() { - return started; - } + void setStarted(int started); - public void setStarted(int started) { - this.started = started; - } + int getLastSessionRefresh(); - public int getLastSessionRefresh() { - return lastSessionRefresh; - } + void setLastSessionRefresh(int lastSessionRefresh); - public void setLastSessionRefresh(int lastSessionRefresh) { - this.lastSessionRefresh = lastSessionRefresh; - } + boolean isOffline(); - public boolean isOffline() { - return offline; - } + void setOffline(boolean offline); - public void setOffline(boolean offline) { - this.offline = offline; - } + String getData(); - public String getData() { - return data; - } + void setData(String data); + + void setRealmId(String realmId); + + void setUserId(String userId); + + void setBrokerSessionId(String brokerSessionId); - public void setData(String data) { - this.data = data; - } } diff --git a/model/storage-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/model/storage-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java index c254981923..6829b81735 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java +++ b/model/storage-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java @@ -86,6 +86,10 @@ public interface UserSessionPersisterProvider extends Provider { */ Stream loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults); + default UserSessionModel loadUserSessionsStreamByBrokerSessionId(RealmModel realm, String brokerSessionId, boolean offline) { + throw new IllegalStateException("not implemented"); + } + /** * Called during startup. For each userSession, it loads also clientSessions. * @param firstResult {@code Integer} Index of the first desired user session. Ignored if negative or {@code null}. @@ -135,4 +139,12 @@ public interface UserSessionPersisterProvider extends Provider { */ Map getUserSessionsCountsByClients(RealmModel realm, boolean offline); + /** + * Remove the online user sessions for this realm. + */ + default void removeUserSessions(RealmModel realm, boolean offline) { + throw new IllegalArgumentException("not supported"); + // TODO: remove default implementation + } + } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java index bb75fae7dd..4533702cd4 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.keycloak.common.Profile; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.RawDistOnly; @@ -16,6 +17,9 @@ import org.keycloak.quarkus.runtime.cli.command.Build; import org.keycloak.quarkus.runtime.cli.command.Start; import org.keycloak.quarkus.runtime.cli.command.StartDev; +import java.util.Arrays; +import java.util.stream.Collectors; + import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,7 +30,11 @@ import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTI @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class FeaturesDistTest { - private static final String PREVIEW_FEATURES_EXPECTED_LOG = "Preview features enabled: admin-fine-grained-authz:v1, client-secret-rotation:v1, dpop:v1, recovery-codes:v1, scripts:v1, token-exchange:v1, update-email:v1"; + private static final String PREVIEW_FEATURES_EXPECTED_LOG = "Preview features enabled: " + Arrays.stream(Profile.Feature.values()) + .filter(feature -> feature.getType() == Profile.Feature.Type.PREVIEW) + .map(Profile.Feature::getVersionedKey) + .sorted() + .collect(Collectors.joining(", ")); @Test public void testEnableOnBuild(KeycloakDistribution dist) { @@ -102,6 +110,7 @@ public class FeaturesDistTest { } private void assertPreviewFeaturesEnabled(CLIResult result) { + assertThat("expecting at least one preview feature on the list", PREVIEW_FEATURES_EXPECTED_LOG, containsString(":")); assertThat(result.getOutput(), CoreMatchers.allOf( containsString(PREVIEW_FEATURES_EXPECTED_LOG))); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java b/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java index 7ffae7d7d6..03b37ee61e 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java @@ -24,4 +24,6 @@ package org.keycloak.models; */ public interface OfflineUserSessionModel extends UserSessionModel { public String getUserId(); + + void setLoginUsername(String loginUsername); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RealmModelDelegate.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RealmModelDelegate.java new file mode 100644 index 0000000000..6353ef991d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RealmModelDelegate.java @@ -0,0 +1,1123 @@ +/* + * Copyright 2024 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.utils; + +import org.keycloak.common.enums.SslRequired; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.CibaConfig; +import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.OAuth2DeviceConfig; +import org.keycloak.models.OTPPolicy; +import org.keycloak.models.ParConfig; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.WebAuthnPolicy; +import org.keycloak.provider.Provider; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +/** + * @author Alexander Schwartz + */ +public class RealmModelDelegate implements RealmModel { + private RealmModel delegate; + + public RealmModelDelegate(RealmModel delegate) { + this.delegate = delegate; + } + + public String getId() { + return delegate.getId(); + } + + public String getName() { + return delegate.getName(); + } + + public void setName(String name) { + delegate.setName(name); + } + + public String getDisplayName() { + return delegate.getDisplayName(); + } + + public void setDisplayName(String displayName) { + delegate.setDisplayName(displayName); + } + + public String getDisplayNameHtml() { + return delegate.getDisplayNameHtml(); + } + + public void setDisplayNameHtml(String displayNameHtml) { + delegate.setDisplayNameHtml(displayNameHtml); + } + + public boolean isEnabled() { + return delegate.isEnabled(); + } + + public void setEnabled(boolean enabled) { + delegate.setEnabled(enabled); + } + + public SslRequired getSslRequired() { + return delegate.getSslRequired(); + } + + public void setSslRequired(SslRequired sslRequired) { + delegate.setSslRequired(sslRequired); + } + + public boolean isRegistrationAllowed() { + return delegate.isRegistrationAllowed(); + } + + public void setRegistrationAllowed(boolean registrationAllowed) { + delegate.setRegistrationAllowed(registrationAllowed); + } + + public boolean isRegistrationEmailAsUsername() { + return delegate.isRegistrationEmailAsUsername(); + } + + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + delegate.setRegistrationEmailAsUsername(registrationEmailAsUsername); + } + + public boolean isRememberMe() { + return delegate.isRememberMe(); + } + + public void setRememberMe(boolean rememberMe) { + delegate.setRememberMe(rememberMe); + } + + public boolean isEditUsernameAllowed() { + return delegate.isEditUsernameAllowed(); + } + + public void setEditUsernameAllowed(boolean editUsernameAllowed) { + delegate.setEditUsernameAllowed(editUsernameAllowed); + } + + public boolean isUserManagedAccessAllowed() { + return delegate.isUserManagedAccessAllowed(); + } + + public void setUserManagedAccessAllowed(boolean userManagedAccessAllowed) { + delegate.setUserManagedAccessAllowed(userManagedAccessAllowed); + } + + public void setAttribute(String name, String value) { + delegate.setAttribute(name, value); + } + + public void setAttribute(String name, Boolean value) { + delegate.setAttribute(name, value); + } + + public void setAttribute(String name, Integer value) { + delegate.setAttribute(name, value); + } + + public void setAttribute(String name, Long value) { + delegate.setAttribute(name, value); + } + + public void removeAttribute(String name) { + delegate.removeAttribute(name); + } + + public String getAttribute(String name) { + return delegate.getAttribute(name); + } + + public Integer getAttribute(String name, Integer defaultValue) { + return delegate.getAttribute(name, defaultValue); + } + + public Long getAttribute(String name, Long defaultValue) { + return delegate.getAttribute(name, defaultValue); + } + + public Boolean getAttribute(String name, Boolean defaultValue) { + return delegate.getAttribute(name, defaultValue); + } + + public Map getAttributes() { + return delegate.getAttributes(); + } + + public boolean isBruteForceProtected() { + return delegate.isBruteForceProtected(); + } + + public void setBruteForceProtected(boolean value) { + delegate.setBruteForceProtected(value); + } + + public boolean isPermanentLockout() { + return delegate.isPermanentLockout(); + } + + public void setPermanentLockout(boolean val) { + delegate.setPermanentLockout(val); + } + + public int getMaxTemporaryLockouts() { + return delegate.getMaxTemporaryLockouts(); + } + + public void setMaxTemporaryLockouts(int val) { + delegate.setMaxTemporaryLockouts(val); + } + + public int getMaxFailureWaitSeconds() { + return delegate.getMaxFailureWaitSeconds(); + } + + public void setMaxFailureWaitSeconds(int val) { + delegate.setMaxFailureWaitSeconds(val); + } + + public int getWaitIncrementSeconds() { + return delegate.getWaitIncrementSeconds(); + } + + public void setWaitIncrementSeconds(int val) { + delegate.setWaitIncrementSeconds(val); + } + + public int getMinimumQuickLoginWaitSeconds() { + return delegate.getMinimumQuickLoginWaitSeconds(); + } + + public void setMinimumQuickLoginWaitSeconds(int val) { + delegate.setMinimumQuickLoginWaitSeconds(val); + } + + public long getQuickLoginCheckMilliSeconds() { + return delegate.getQuickLoginCheckMilliSeconds(); + } + + public void setQuickLoginCheckMilliSeconds(long val) { + delegate.setQuickLoginCheckMilliSeconds(val); + } + + public int getMaxDeltaTimeSeconds() { + return delegate.getMaxDeltaTimeSeconds(); + } + + public void setMaxDeltaTimeSeconds(int val) { + delegate.setMaxDeltaTimeSeconds(val); + } + + public int getFailureFactor() { + return delegate.getFailureFactor(); + } + + public void setFailureFactor(int failureFactor) { + delegate.setFailureFactor(failureFactor); + } + + public boolean isVerifyEmail() { + return delegate.isVerifyEmail(); + } + + public void setVerifyEmail(boolean verifyEmail) { + delegate.setVerifyEmail(verifyEmail); + } + + public boolean isLoginWithEmailAllowed() { + return delegate.isLoginWithEmailAllowed(); + } + + public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) { + delegate.setLoginWithEmailAllowed(loginWithEmailAllowed); + } + + public boolean isDuplicateEmailsAllowed() { + return delegate.isDuplicateEmailsAllowed(); + } + + public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) { + delegate.setDuplicateEmailsAllowed(duplicateEmailsAllowed); + } + + public boolean isResetPasswordAllowed() { + return delegate.isResetPasswordAllowed(); + } + + public void setResetPasswordAllowed(boolean resetPasswordAllowed) { + delegate.setResetPasswordAllowed(resetPasswordAllowed); + } + + public String getDefaultSignatureAlgorithm() { + return delegate.getDefaultSignatureAlgorithm(); + } + + public void setDefaultSignatureAlgorithm(String defaultSignatureAlgorithm) { + delegate.setDefaultSignatureAlgorithm(defaultSignatureAlgorithm); + } + + public boolean isRevokeRefreshToken() { + return delegate.isRevokeRefreshToken(); + } + + public void setRevokeRefreshToken(boolean revokeRefreshToken) { + delegate.setRevokeRefreshToken(revokeRefreshToken); + } + + public int getRefreshTokenMaxReuse() { + return delegate.getRefreshTokenMaxReuse(); + } + + public void setRefreshTokenMaxReuse(int revokeRefreshTokenCount) { + delegate.setRefreshTokenMaxReuse(revokeRefreshTokenCount); + } + + public int getSsoSessionIdleTimeout() { + return delegate.getSsoSessionIdleTimeout(); + } + + public void setSsoSessionIdleTimeout(int seconds) { + delegate.setSsoSessionIdleTimeout(seconds); + } + + public int getSsoSessionMaxLifespan() { + return delegate.getSsoSessionMaxLifespan(); + } + + public void setSsoSessionMaxLifespan(int seconds) { + delegate.setSsoSessionMaxLifespan(seconds); + } + + public int getSsoSessionIdleTimeoutRememberMe() { + return delegate.getSsoSessionIdleTimeoutRememberMe(); + } + + public void setSsoSessionIdleTimeoutRememberMe(int seconds) { + delegate.setSsoSessionIdleTimeoutRememberMe(seconds); + } + + public int getSsoSessionMaxLifespanRememberMe() { + return delegate.getSsoSessionMaxLifespanRememberMe(); + } + + public void setSsoSessionMaxLifespanRememberMe(int seconds) { + delegate.setSsoSessionMaxLifespanRememberMe(seconds); + } + + public int getOfflineSessionIdleTimeout() { + return delegate.getOfflineSessionIdleTimeout(); + } + + public void setOfflineSessionIdleTimeout(int seconds) { + delegate.setOfflineSessionIdleTimeout(seconds); + } + + public int getAccessTokenLifespan() { + return delegate.getAccessTokenLifespan(); + } + + public boolean isOfflineSessionMaxLifespanEnabled() { + return delegate.isOfflineSessionMaxLifespanEnabled(); + } + + public void setOfflineSessionMaxLifespanEnabled(boolean offlineSessionMaxLifespanEnabled) { + delegate.setOfflineSessionMaxLifespanEnabled(offlineSessionMaxLifespanEnabled); + } + + public int getOfflineSessionMaxLifespan() { + return delegate.getOfflineSessionMaxLifespan(); + } + + public void setOfflineSessionMaxLifespan(int seconds) { + delegate.setOfflineSessionMaxLifespan(seconds); + } + + public int getClientSessionIdleTimeout() { + return delegate.getClientSessionIdleTimeout(); + } + + public void setClientSessionIdleTimeout(int seconds) { + delegate.setClientSessionIdleTimeout(seconds); + } + + public int getClientSessionMaxLifespan() { + return delegate.getClientSessionMaxLifespan(); + } + + public void setClientSessionMaxLifespan(int seconds) { + delegate.setClientSessionMaxLifespan(seconds); + } + + public int getClientOfflineSessionIdleTimeout() { + return delegate.getClientOfflineSessionIdleTimeout(); + } + + public void setClientOfflineSessionIdleTimeout(int seconds) { + delegate.setClientOfflineSessionIdleTimeout(seconds); + } + + public int getClientOfflineSessionMaxLifespan() { + return delegate.getClientOfflineSessionMaxLifespan(); + } + + public void setClientOfflineSessionMaxLifespan(int seconds) { + delegate.setClientOfflineSessionMaxLifespan(seconds); + } + + public void setAccessTokenLifespan(int seconds) { + delegate.setAccessTokenLifespan(seconds); + } + + public int getAccessTokenLifespanForImplicitFlow() { + return delegate.getAccessTokenLifespanForImplicitFlow(); + } + + public void setAccessTokenLifespanForImplicitFlow(int seconds) { + delegate.setAccessTokenLifespanForImplicitFlow(seconds); + } + + public int getAccessCodeLifespan() { + return delegate.getAccessCodeLifespan(); + } + + public void setAccessCodeLifespan(int seconds) { + delegate.setAccessCodeLifespan(seconds); + } + + public int getAccessCodeLifespanUserAction() { + return delegate.getAccessCodeLifespanUserAction(); + } + + public void setAccessCodeLifespanUserAction(int seconds) { + delegate.setAccessCodeLifespanUserAction(seconds); + } + + public OAuth2DeviceConfig getOAuth2DeviceConfig() { + return delegate.getOAuth2DeviceConfig(); + } + + public CibaConfig getCibaPolicy() { + return delegate.getCibaPolicy(); + } + + public ParConfig getParPolicy() { + return delegate.getParPolicy(); + } + + public Map getUserActionTokenLifespans() { + return delegate.getUserActionTokenLifespans(); + } + + public int getAccessCodeLifespanLogin() { + return delegate.getAccessCodeLifespanLogin(); + } + + public void setAccessCodeLifespanLogin(int seconds) { + delegate.setAccessCodeLifespanLogin(seconds); + } + + public int getActionTokenGeneratedByAdminLifespan() { + return delegate.getActionTokenGeneratedByAdminLifespan(); + } + + public void setActionTokenGeneratedByAdminLifespan(int seconds) { + delegate.setActionTokenGeneratedByAdminLifespan(seconds); + } + + public int getActionTokenGeneratedByUserLifespan() { + return delegate.getActionTokenGeneratedByUserLifespan(); + } + + public void setActionTokenGeneratedByUserLifespan(int seconds) { + delegate.setActionTokenGeneratedByUserLifespan(seconds); + } + + public int getActionTokenGeneratedByUserLifespan(String actionTokenType) { + return delegate.getActionTokenGeneratedByUserLifespan(actionTokenType); + } + + public void setActionTokenGeneratedByUserLifespan(String actionTokenType, Integer seconds) { + delegate.setActionTokenGeneratedByUserLifespan(actionTokenType, seconds); + } + + public Stream getRequiredCredentialsStream() { + return delegate.getRequiredCredentialsStream(); + } + + public void addRequiredCredential(String cred) { + delegate.addRequiredCredential(cred); + } + + public PasswordPolicy getPasswordPolicy() { + return delegate.getPasswordPolicy(); + } + + public void setPasswordPolicy(PasswordPolicy policy) { + delegate.setPasswordPolicy(policy); + } + + public OTPPolicy getOTPPolicy() { + return delegate.getOTPPolicy(); + } + + public void setOTPPolicy(OTPPolicy policy) { + delegate.setOTPPolicy(policy); + } + + public WebAuthnPolicy getWebAuthnPolicy() { + return delegate.getWebAuthnPolicy(); + } + + public void setWebAuthnPolicy(WebAuthnPolicy policy) { + delegate.setWebAuthnPolicy(policy); + } + + public WebAuthnPolicy getWebAuthnPolicyPasswordless() { + return delegate.getWebAuthnPolicyPasswordless(); + } + + public void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy) { + delegate.setWebAuthnPolicyPasswordless(policy); + } + + public RoleModel getRoleById(String id) { + return delegate.getRoleById(id); + } + + public Stream getDefaultGroupsStream() { + return delegate.getDefaultGroupsStream(); + } + + public void addDefaultGroup(GroupModel group) { + delegate.addDefaultGroup(group); + } + + public void removeDefaultGroup(GroupModel group) { + delegate.removeDefaultGroup(group); + } + + public Stream getClientsStream() { + return delegate.getClientsStream(); + } + + public Stream getClientsStream(Integer firstResult, Integer maxResults) { + return delegate.getClientsStream(firstResult, maxResults); + } + + public Long getClientsCount() { + return delegate.getClientsCount(); + } + + public Stream getAlwaysDisplayInConsoleClientsStream() { + return delegate.getAlwaysDisplayInConsoleClientsStream(); + } + + public ClientModel addClient(String name) { + return delegate.addClient(name); + } + + public ClientModel addClient(String id, String clientId) { + return delegate.addClient(id, clientId); + } + + public boolean removeClient(String id) { + return delegate.removeClient(id); + } + + public ClientModel getClientById(String id) { + return delegate.getClientById(id); + } + + public ClientModel getClientByClientId(String clientId) { + return delegate.getClientByClientId(clientId); + } + + public Stream searchClientByClientIdStream(String clientId, Integer firstResult, Integer maxResults) { + return delegate.searchClientByClientIdStream(clientId, firstResult, maxResults); + } + + public Stream searchClientByAttributes(Map attributes, Integer firstResult, Integer maxResults) { + return delegate.searchClientByAttributes(attributes, firstResult, maxResults); + } + + public Stream searchClientByAuthenticationFlowBindingOverrides(Map overrides, Integer firstResult, Integer maxResults) { + return delegate.searchClientByAuthenticationFlowBindingOverrides(overrides, firstResult, maxResults); + } + + public void updateRequiredCredentials(Set creds) { + delegate.updateRequiredCredentials(creds); + } + + public Map getBrowserSecurityHeaders() { + return delegate.getBrowserSecurityHeaders(); + } + + public void setBrowserSecurityHeaders(Map headers) { + delegate.setBrowserSecurityHeaders(headers); + } + + public Map getSmtpConfig() { + return delegate.getSmtpConfig(); + } + + public void setSmtpConfig(Map smtpConfig) { + delegate.setSmtpConfig(smtpConfig); + } + + public AuthenticationFlowModel getBrowserFlow() { + return delegate.getBrowserFlow(); + } + + public void setBrowserFlow(AuthenticationFlowModel flow) { + delegate.setBrowserFlow(flow); + } + + public AuthenticationFlowModel getRegistrationFlow() { + return delegate.getRegistrationFlow(); + } + + public void setRegistrationFlow(AuthenticationFlowModel flow) { + delegate.setRegistrationFlow(flow); + } + + public AuthenticationFlowModel getDirectGrantFlow() { + return delegate.getDirectGrantFlow(); + } + + public void setDirectGrantFlow(AuthenticationFlowModel flow) { + delegate.setDirectGrantFlow(flow); + } + + public AuthenticationFlowModel getResetCredentialsFlow() { + return delegate.getResetCredentialsFlow(); + } + + public void setResetCredentialsFlow(AuthenticationFlowModel flow) { + delegate.setResetCredentialsFlow(flow); + } + + public AuthenticationFlowModel getClientAuthenticationFlow() { + return delegate.getClientAuthenticationFlow(); + } + + public void setClientAuthenticationFlow(AuthenticationFlowModel flow) { + delegate.setClientAuthenticationFlow(flow); + } + + public AuthenticationFlowModel getDockerAuthenticationFlow() { + return delegate.getDockerAuthenticationFlow(); + } + + public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) { + delegate.setDockerAuthenticationFlow(flow); + } + + public AuthenticationFlowModel getFirstBrokerLoginFlow() { + return delegate.getFirstBrokerLoginFlow(); + } + + public void setFirstBrokerLoginFlow(AuthenticationFlowModel flow) { + delegate.setFirstBrokerLoginFlow(flow); + } + + public Stream getAuthenticationFlowsStream() { + return delegate.getAuthenticationFlowsStream(); + } + + public AuthenticationFlowModel getFlowByAlias(String alias) { + return delegate.getFlowByAlias(alias); + } + + public AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model) { + return delegate.addAuthenticationFlow(model); + } + + public AuthenticationFlowModel getAuthenticationFlowById(String id) { + return delegate.getAuthenticationFlowById(id); + } + + public void removeAuthenticationFlow(AuthenticationFlowModel model) { + delegate.removeAuthenticationFlow(model); + } + + public void updateAuthenticationFlow(AuthenticationFlowModel model) { + delegate.updateAuthenticationFlow(model); + } + + public Stream getAuthenticationExecutionsStream(String flowId) { + return delegate.getAuthenticationExecutionsStream(flowId); + } + + public AuthenticationExecutionModel getAuthenticationExecutionById(String id) { + return delegate.getAuthenticationExecutionById(id); + } + + public AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId) { + return delegate.getAuthenticationExecutionByFlowId(flowId); + } + + public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) { + return delegate.addAuthenticatorExecution(model); + } + + public void updateAuthenticatorExecution(AuthenticationExecutionModel model) { + delegate.updateAuthenticatorExecution(model); + } + + public void removeAuthenticatorExecution(AuthenticationExecutionModel model) { + delegate.removeAuthenticatorExecution(model); + } + + public Stream getAuthenticatorConfigsStream() { + return delegate.getAuthenticatorConfigsStream(); + } + + public AuthenticatorConfigModel addAuthenticatorConfig(AuthenticatorConfigModel model) { + return delegate.addAuthenticatorConfig(model); + } + + public void updateAuthenticatorConfig(AuthenticatorConfigModel model) { + delegate.updateAuthenticatorConfig(model); + } + + public void removeAuthenticatorConfig(AuthenticatorConfigModel model) { + delegate.removeAuthenticatorConfig(model); + } + + public AuthenticatorConfigModel getAuthenticatorConfigById(String id) { + return delegate.getAuthenticatorConfigById(id); + } + + public AuthenticatorConfigModel getAuthenticatorConfigByAlias(String alias) { + return delegate.getAuthenticatorConfigByAlias(alias); + } + + public Stream getRequiredActionProvidersStream() { + return delegate.getRequiredActionProvidersStream(); + } + + public RequiredActionProviderModel addRequiredActionProvider(RequiredActionProviderModel model) { + return delegate.addRequiredActionProvider(model); + } + + public void updateRequiredActionProvider(RequiredActionProviderModel model) { + delegate.updateRequiredActionProvider(model); + } + + public void removeRequiredActionProvider(RequiredActionProviderModel model) { + delegate.removeRequiredActionProvider(model); + } + + public RequiredActionProviderModel getRequiredActionProviderById(String id) { + return delegate.getRequiredActionProviderById(id); + } + + public RequiredActionProviderModel getRequiredActionProviderByAlias(String alias) { + return delegate.getRequiredActionProviderByAlias(alias); + } + + public Stream getIdentityProvidersStream() { + return delegate.getIdentityProvidersStream(); + } + + public IdentityProviderModel getIdentityProviderByAlias(String alias) { + return delegate.getIdentityProviderByAlias(alias); + } + + public void addIdentityProvider(IdentityProviderModel identityProvider) { + delegate.addIdentityProvider(identityProvider); + } + + public void removeIdentityProviderByAlias(String alias) { + delegate.removeIdentityProviderByAlias(alias); + } + + public void updateIdentityProvider(IdentityProviderModel identityProvider) { + delegate.updateIdentityProvider(identityProvider); + } + + public Stream getIdentityProviderMappersStream() { + return delegate.getIdentityProviderMappersStream(); + } + + public Stream getIdentityProviderMappersByAliasStream(String brokerAlias) { + return delegate.getIdentityProviderMappersByAliasStream(brokerAlias); + } + + public IdentityProviderMapperModel addIdentityProviderMapper(IdentityProviderMapperModel model) { + return delegate.addIdentityProviderMapper(model); + } + + public void removeIdentityProviderMapper(IdentityProviderMapperModel mapping) { + delegate.removeIdentityProviderMapper(mapping); + } + + public void updateIdentityProviderMapper(IdentityProviderMapperModel mapping) { + delegate.updateIdentityProviderMapper(mapping); + } + + public IdentityProviderMapperModel getIdentityProviderMapperById(String id) { + return delegate.getIdentityProviderMapperById(id); + } + + public IdentityProviderMapperModel getIdentityProviderMapperByName(String brokerAlias, String name) { + return delegate.getIdentityProviderMapperByName(brokerAlias, name); + } + + public ComponentModel addComponentModel(ComponentModel model) { + return delegate.addComponentModel(model); + } + + public ComponentModel importComponentModel(ComponentModel model) { + return delegate.importComponentModel(model); + } + + public void updateComponent(ComponentModel component) { + delegate.updateComponent(component); + } + + public void removeComponent(ComponentModel component) { + delegate.removeComponent(component); + } + + public void removeComponents(String parentId) { + delegate.removeComponents(parentId); + } + + public Stream getComponentsStream(String parentId, String providerType) { + return delegate.getComponentsStream(parentId, providerType); + } + + public Stream getComponentsStream(String parentId) { + return delegate.getComponentsStream(parentId); + } + + public Stream getComponentsStream() { + return delegate.getComponentsStream(); + } + + public ComponentModel getComponent(String id) { + return delegate.getComponent(id); + } + + public Stream getStorageProviders(Class storageProviderClass) { + return delegate.getStorageProviders(storageProviderClass); + } + + public String getLoginTheme() { + return delegate.getLoginTheme(); + } + + public void setLoginTheme(String name) { + delegate.setLoginTheme(name); + } + + public String getAccountTheme() { + return delegate.getAccountTheme(); + } + + public void setAccountTheme(String name) { + delegate.setAccountTheme(name); + } + + public String getAdminTheme() { + return delegate.getAdminTheme(); + } + + public void setAdminTheme(String name) { + delegate.setAdminTheme(name); + } + + public String getEmailTheme() { + return delegate.getEmailTheme(); + } + + public void setEmailTheme(String name) { + delegate.setEmailTheme(name); + } + + public int getNotBefore() { + return delegate.getNotBefore(); + } + + public void setNotBefore(int notBefore) { + delegate.setNotBefore(notBefore); + } + + public boolean isEventsEnabled() { + return delegate.isEventsEnabled(); + } + + public void setEventsEnabled(boolean enabled) { + delegate.setEventsEnabled(enabled); + } + + public long getEventsExpiration() { + return delegate.getEventsExpiration(); + } + + public void setEventsExpiration(long expiration) { + delegate.setEventsExpiration(expiration); + } + + public Stream getEventsListenersStream() { + return delegate.getEventsListenersStream(); + } + + public void setEventsListeners(Set listeners) { + delegate.setEventsListeners(listeners); + } + + public Stream getEnabledEventTypesStream() { + return delegate.getEnabledEventTypesStream(); + } + + public void setEnabledEventTypes(Set enabledEventTypes) { + delegate.setEnabledEventTypes(enabledEventTypes); + } + + public boolean isAdminEventsEnabled() { + return delegate.isAdminEventsEnabled(); + } + + public void setAdminEventsEnabled(boolean enabled) { + delegate.setAdminEventsEnabled(enabled); + } + + public boolean isAdminEventsDetailsEnabled() { + return delegate.isAdminEventsDetailsEnabled(); + } + + public void setAdminEventsDetailsEnabled(boolean enabled) { + delegate.setAdminEventsDetailsEnabled(enabled); + } + + public ClientModel getMasterAdminClient() { + return delegate.getMasterAdminClient(); + } + + public void setMasterAdminClient(ClientModel client) { + delegate.setMasterAdminClient(client); + } + + public RoleModel getDefaultRole() { + return delegate.getDefaultRole(); + } + + public void setDefaultRole(RoleModel role) { + delegate.setDefaultRole(role); + } + + public boolean isIdentityFederationEnabled() { + return delegate.isIdentityFederationEnabled(); + } + + public boolean isInternationalizationEnabled() { + return delegate.isInternationalizationEnabled(); + } + + public void setInternationalizationEnabled(boolean enabled) { + delegate.setInternationalizationEnabled(enabled); + } + + public Stream getSupportedLocalesStream() { + return delegate.getSupportedLocalesStream(); + } + + public void setSupportedLocales(Set locales) { + delegate.setSupportedLocales(locales); + } + + public String getDefaultLocale() { + return delegate.getDefaultLocale(); + } + + public void setDefaultLocale(String locale) { + delegate.setDefaultLocale(locale); + } + + public GroupModel createGroup(String name) { + return delegate.createGroup(name); + } + + public GroupModel createGroup(String id, String name) { + return delegate.createGroup(id, name); + } + + public GroupModel createGroup(String name, GroupModel toParent) { + return delegate.createGroup(name, toParent); + } + + public GroupModel createGroup(String id, String name, GroupModel toParent) { + return delegate.createGroup(id, name, toParent); + } + + public GroupModel getGroupById(String id) { + return delegate.getGroupById(id); + } + + public Stream getGroupsStream() { + return delegate.getGroupsStream(); + } + + public Long getGroupsCount(Boolean onlyTopGroups) { + return delegate.getGroupsCount(onlyTopGroups); + } + + public Long getGroupsCountByNameContaining(String search) { + return delegate.getGroupsCountByNameContaining(search); + } + + @Deprecated + public Stream getTopLevelGroupsStream() { + return delegate.getTopLevelGroupsStream(); + } + + @Deprecated + public Stream getTopLevelGroupsStream(Integer first, Integer max) { + return delegate.getTopLevelGroupsStream(first, max); + } + + public boolean removeGroup(GroupModel group) { + return delegate.removeGroup(group); + } + + public void moveGroup(GroupModel group, GroupModel toParent) { + delegate.moveGroup(group, toParent); + } + + public Stream getClientScopesStream() { + return delegate.getClientScopesStream(); + } + + public ClientScopeModel addClientScope(String name) { + return delegate.addClientScope(name); + } + + public ClientScopeModel addClientScope(String id, String name) { + return delegate.addClientScope(id, name); + } + + public boolean removeClientScope(String id) { + return delegate.removeClientScope(id); + } + + public ClientScopeModel getClientScopeById(String id) { + return delegate.getClientScopeById(id); + } + + public void addDefaultClientScope(ClientScopeModel clientScope, boolean defaultScope) { + delegate.addDefaultClientScope(clientScope, defaultScope); + } + + public void removeDefaultClientScope(ClientScopeModel clientScope) { + delegate.removeDefaultClientScope(clientScope); + } + + public void createOrUpdateRealmLocalizationTexts(String locale, Map localizationTexts) { + delegate.createOrUpdateRealmLocalizationTexts(locale, localizationTexts); + } + + public boolean removeRealmLocalizationTexts(String locale) { + return delegate.removeRealmLocalizationTexts(locale); + } + + public Map> getRealmLocalizationTexts() { + return delegate.getRealmLocalizationTexts(); + } + + public Map getRealmLocalizationTextsByLocale(String locale) { + return delegate.getRealmLocalizationTextsByLocale(locale); + } + + public Stream getDefaultClientScopesStream(boolean defaultScope) { + return delegate.getDefaultClientScopesStream(defaultScope); + } + + public void addToDefaultRoles(RoleModel role) { + delegate.addToDefaultRoles(role); + } + + public ClientInitialAccessModel createClientInitialAccessModel(int expiration, int count) { + return delegate.createClientInitialAccessModel(expiration, count); + } + + public ClientInitialAccessModel getClientInitialAccessModel(String id) { + return delegate.getClientInitialAccessModel(id); + } + + public void removeClientInitialAccessModel(String id) { + delegate.removeClientInitialAccessModel(id); + } + + public Stream getClientInitialAccesses() { + return delegate.getClientInitialAccesses(); + } + + public void decreaseRemainingCount(ClientInitialAccessModel clientInitialAccess) { + delegate.decreaseRemainingCount(clientInitialAccess); + } + + public RoleModel getRole(String name) { + return delegate.getRole(name); + } + + public RoleModel addRole(String name) { + return delegate.addRole(name); + } + + public RoleModel addRole(String id, String name) { + return delegate.addRole(id, name); + } + + public boolean removeRole(RoleModel role) { + return delegate.removeRole(role); + } + + public Stream getRolesStream() { + return delegate.getRolesStream(); + } + + public Stream getRolesStream(Integer firstResult, Integer maxResults) { + return delegate.getRolesStream(firstResult, maxResults); + } + + public Stream searchForRolesStream(String search, Integer first, Integer max) { + return delegate.searchForRolesStream(search, first, max); + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/UserSessionModelDelegate.java b/server-spi-private/src/main/java/org/keycloak/models/utils/UserSessionModelDelegate.java new file mode 100644 index 0000000000..fe6d8c751d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/UserSessionModelDelegate.java @@ -0,0 +1,134 @@ +/* + * Copyright 2024 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.utils; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; + +import java.util.Collection; +import java.util.Map; + +/** + * @author Alexander Schwartz + */ +public class UserSessionModelDelegate implements UserSessionModel { + private UserSessionModel delegate; + + public UserSessionModelDelegate(UserSessionModel delegate) { + this.delegate = delegate; + } + + public String getId() { + return delegate.getId(); + } + + public RealmModel getRealm() { + return delegate.getRealm(); + } + + public String getBrokerSessionId() { + return delegate.getBrokerSessionId(); + } + + public String getBrokerUserId() { + return delegate.getBrokerUserId(); + } + + public UserModel getUser() { + return delegate.getUser(); + } + + public String getLoginUsername() { + return delegate.getLoginUsername(); + } + + public String getIpAddress() { + return delegate.getIpAddress(); + } + + public String getAuthMethod() { + return delegate.getAuthMethod(); + } + + public boolean isRememberMe() { + return delegate.isRememberMe(); + } + + public int getStarted() { + return delegate.getStarted(); + } + + public int getLastSessionRefresh() { + return delegate.getLastSessionRefresh(); + } + + public void setLastSessionRefresh(int seconds) { + delegate.setLastSessionRefresh(seconds); + } + + public boolean isOffline() { + return delegate.isOffline(); + } + + public Map getAuthenticatedClientSessions() { + return delegate.getAuthenticatedClientSessions(); + } + + public AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) { + return delegate.getAuthenticatedClientSessionByClient(clientUUID); + } + + public void removeAuthenticatedClientSessions(Collection removedClientUUIDS) { + delegate.removeAuthenticatedClientSessions(removedClientUUIDS); + } + + public String getNote(String name) { + return delegate.getNote(name); + } + + public void setNote(String name, String value) { + delegate.setNote(name, value); + } + + public void removeNote(String name) { + delegate.removeNote(name); + } + + public Map getNotes() { + return delegate.getNotes(); + } + + public UserSessionModel.State getState() { + return delegate.getState(); + } + + public void setState(UserSessionModel.State state) { + delegate.setState(state); + } + + public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + delegate.restartSession(realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); + } + + public UserSessionModel.SessionPersistenceState getPersistenceState() { + return delegate.getPersistenceState(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java index 67ef4456e3..deb57ee126 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java @@ -193,8 +193,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo // only run build during first execution of the server (if the DB is specified), restarts or when running cluster tests if (restart.get() || "ha".equals(cacheMode) || shouldSetUpDb.get() || configuration.getFipsMode() != FipsMode.DISABLED) { - commands.removeIf("--optimized"::equals); - commands.add("--http-relative-path=/auth"); + prepareCommandsForRebuilding(commands); if ("local".equals(cacheMode)) { commands.add("--cache=local"); @@ -213,6 +212,15 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo return commands; } + /** + * When enabling automatic rebuilding of the image, the `--optimized` argument must be removed, + * and all original build time parameters must be added. + */ + private static void prepareCommandsForRebuilding(List commands) { + commands.removeIf("--optimized"::equals); + commands.add("--http-relative-path=/auth"); + } + protected void addFeaturesOption(List commands) { String defaultFeatures = configuration.getDefaultFeatures(); @@ -238,6 +246,9 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo } } + // enabling or disabling features requires rebuilding the image + prepareCommandsForRebuilding(commands); + commands.add(featuresOption.toString()); } 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 c956c856df..ec7b23ac0d 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 @@ -304,6 +304,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { String userSessionId = KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> { RealmModel realm = kcSession.realms().getRealmByName("test"); UserSessionModel userSession = createSessions(kcSession)[0]; + userSession = kcSession.sessions().getUserSession(realm, userSession.getId()); kcSession.sessions().removeUserSession(realm, userSession); return userSession.getId(); @@ -559,9 +560,11 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { public void testRemovingExpiredSession(KeycloakSession session) { UserSessionModel[] sessions = createSessions(session); try { - Time.setOffset(3600000); UserSessionModel userSession = sessions[0]; RealmModel realm = userSession.getRealm(); + // reload userSession in current session + userSession = session.sessions().getUserSession(realm, userSession.getId()); + Time.setOffset(3600000); session.sessions().removeExpired(realm); // Assert no exception is thrown here 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 b50603c274..0a94ed0a32 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 @@ -522,6 +522,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest { // Refresh with the offline token tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1"); + Assert.assertNull("received error " + tokenResponse.getError() + ", " + tokenResponse.getErrorDescription(), tokenResponse.getError()); // Use accessToken to admin REST request try (Keycloak offlineTokenAdmin = Keycloak.getInstance(getAuthServerContextRoot() + "/auth", diff --git a/testsuite/integration-arquillian/tests/base/testsuites/persistent-sessions-suite b/testsuite/integration-arquillian/tests/base/testsuites/persistent-sessions-suite new file mode 100644 index 0000000000..bd134a8056 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/testsuites/persistent-sessions-suite @@ -0,0 +1,22 @@ +SessionTest +KcSamlBrokerSessionNotOnOrAfterTest +OidcClaimToUserSessionNoteMapperTest +KcOidcBrokerTransientSessionsTest +KcAdmSessionTest +TransientSessionTest +UserSessionProviderOfflineTest +AuthenticationSessionProviderTest +UserSessionProviderTest +OAuthDanceClientSessionExtensionTest +SessionNotOnOrAfterTest +SessionTimeoutValidationTest +LastSessionRefreshUnitTest +KcOidcUserSessionLimitsBrokerTest +KcSamlUserSessionLimitsBrokerTest +AbstractUserSessionLimitsBrokerTest +UserSessionLimitsTest +ConcurrentLoginTest +RefreshTokenTest +OfflineTokenTest +AccessTokenTest +LogoutTest \ No newline at end of file diff --git a/testsuite/model/pom.xml b/testsuite/model/pom.xml index 80c3265b2c..4dc42c2e60 100644 --- a/testsuite/model/pom.xml +++ b/testsuite/model/pom.xml @@ -212,6 +212,13 @@ + + jpa+infinispan+persistentsessions + + Infinispan,Jpa,PersistentUserSessions + + + jpa+infinispan+client-storage diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/PersistentUserSessions.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/PersistentUserSessions.java new file mode 100644 index 0000000000..a1ae1187e5 --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/PersistentUserSessions.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.model.parameters; + +import org.keycloak.common.Profile; +import org.keycloak.common.profile.PropertiesProfileConfigResolver; +import org.keycloak.testsuite.model.Config; +import org.keycloak.testsuite.model.KeycloakModelParameters; + +import java.util.Collections; + +public class PersistentUserSessions extends KeycloakModelParameters { + + public PersistentUserSessions() { + super(Collections.emptySet(), Collections.emptySet()); + } + + @Override + public void updateConfig(Config cf) { + updateConfigForJpa(cf); + } + + public static void updateConfigForJpa(Config cf) { + System.getProperties().put(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.PERSISTENT_USER_SESSIONS), "enabled"); + System.getProperties().put(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE), "enabled"); + } +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/OfflineSessionPersistenceTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/OfflineSessionPersistenceTest.java index 11e0228d7c..5ded5fbef3 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/OfflineSessionPersistenceTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/OfflineSessionPersistenceTest.java @@ -29,6 +29,7 @@ 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.models.sessions.infinispan.PersistentUserSessionProvider; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.model.KeycloakModelTest; import org.keycloak.testsuite.model.RequireProvider; @@ -258,7 +259,13 @@ public class OfflineSessionPersistenceTest extends KeycloakModelTest { // Delete local user cache (persisted sessions are still kept) UserSessionProvider provider = session.getProvider(UserSessionProvider.class); // Remove in-memory representation of the offline sessions - ((InfinispanUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true); + if (provider instanceof InfinispanUserSessionProvider) { + ((InfinispanUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true); + } else if (provider instanceof PersistentUserSessionProvider) { + ((PersistentUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true); + } else { + throw new IllegalStateException("Unknown UserSessionProvider: " + provider); + } return null; }); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionInitializerTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionInitializerTest.java index 2a9292c628..6fbf46901b 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionInitializerTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionInitializerTest.java @@ -17,10 +17,13 @@ package org.keycloak.testsuite.model.session; +import org.hamcrest.Matchers; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; +import org.keycloak.common.Profile; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -155,6 +158,9 @@ public class UserSessionInitializerTest extends KeycloakModelTest { @Test public void testUserSessionPropagationBetweenSites() throws InterruptedException { + // When user sessions are not stored in the cache, this test doesn't make sense + Assume.assumeThat(Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE), Matchers.not(true)); + AtomicInteger index = new AtomicInteger(); AtomicReference userSessionId = new AtomicReference<>(); AtomicReference> containsSession = new AtomicReference<>(new LinkedList<>()); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java index 2f51d3d62e..271f164548 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite.model.session; import org.junit.Assert; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.common.Profile; import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; @@ -144,18 +145,20 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { .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); - }); + if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + 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 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 @@ -261,7 +264,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { UserSessionModel userSession = session.sessions().createUserSession(null, fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); userSessionID.set(userSession.getId()); - createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); + createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); }); inComittedTransaction(session -> { @@ -302,8 +305,8 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { UserSessionModel userSession = session.sessions().createUserSession(null, fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); 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"); + createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state"); + createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state"); }); inComittedTransaction(session -> { diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java index e04d5cd3ad..516af4fd3f 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java @@ -101,8 +101,8 @@ public class UserSessionProviderModelTest extends KeycloakModelTest { inComittedTransaction(session -> { RealmModel realm = session.realms().getRealm(realmId); - session.sessions().removeUserSession(realm, origSessions[0]); - session.sessions().removeUserSession(realm, origSessions[1]); + session.sessions().removeUserSession(realm, session.sessions().getUserSession(realm, origSessions[0].getId())); + session.sessions().removeUserSession(realm, session.sessions().getUserSession(realm, origSessions[1].getId())); }); inComittedTransaction(session -> { diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java index b48dccdb2d..cd63439bec 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java @@ -24,6 +24,7 @@ import org.infinispan.context.Flag; import org.junit.Assert; import org.junit.Assume; import org.junit.Test; +import org.keycloak.common.Profile; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.AuthenticatedClientSessionModel; @@ -300,10 +301,20 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest { session.sessions().createOfflineUserSession(userSession); session.sessions().createOfflineUserSession(origSessions[0]); - // try to load user session from persister - Assert.assertEquals(2, persister.loadUserSessionsStream(0, 10, true, "00000000-0000-0000-0000-000000000000").count()); + if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + // This does not work with persistent user sessions because we currently have two transactions and the one that creates the offline user sessions is not committing the changes + // try to load user session from persister + Assert.assertEquals(2, persister.loadUserSessionsStream(0, 10, true, "00000000-0000-0000-0000-000000000000").count()); + } }); + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + inComittedTransaction(session -> { + persister = session.getProvider(UserSessionPersisterProvider.class); + Assert.assertEquals(2, persister.loadUserSessionsStream(0, 10, true, "00000000-0000-0000-0000-000000000000").count()); + }); + } + } finally { setTimeOffset(0); kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());