Sessions not removed when user is deleted

Fixes #43323

Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
This commit is contained in:
Pedro Ruivo 2025-11-11 13:09:05 +00:00 committed by GitHub
parent f9fd9bce9e
commit 39964befef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 278 additions and 22 deletions

View File

@ -97,9 +97,11 @@ import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.stream.AuthClientSessionSetMapper;
import org.keycloak.models.sessions.infinispan.stream.ClientSessionFilterByUser;
import org.keycloak.models.sessions.infinispan.stream.CollectionToStreamMapper;
import org.keycloak.models.sessions.infinispan.stream.GroupAndCountCollectorSupplier;
import org.keycloak.models.sessions.infinispan.stream.MapEntryToKeyMapper;
import org.keycloak.models.sessions.infinispan.stream.RemoveKeyConsumer;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.SessionUnwrapMapper;
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
@ -223,6 +225,8 @@ import org.keycloak.storage.managers.UserStorageSyncManager;
GroupAndCountCollectorSupplier.class,
MapEntryToKeyMapper.class,
SessionUnwrapMapper.class,
ClientSessionFilterByUser.class,
RemoveKeyConsumer.class,
// infinispan.module.certificates
ReloadCertificateFunction.class,

View File

@ -45,6 +45,7 @@ import org.infinispan.protostream.SerializationContextInitializer;
* <p>
* Docs: <a href="https://protobuf.dev/programming-guides/proto3/">Language Guide (proto 3)</a>
*/
@SuppressWarnings("unused")
public final class Marshalling {
public static final String PROTO_SCHEMA_PACKAGE = "keycloak";
@ -183,6 +184,8 @@ public final class Marshalling {
public static final int RELOAD_CERTIFICATE_FUNCTION = 65615;
public static final int EMBEDDED_CLIENT_SESSION_KEY = 65616;
public static final int CLIENT_SESSION_USER_FILTER = 65617;
public static final int REMOVE_KEY_BI_CONSUMER = 65618;
public static void configure(GlobalConfigurationBuilder builder) {
getSchemas().forEach(builder.serialization()::addContextInitializer);

View File

@ -906,6 +906,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
entity.setRealmId(realmId);
entity.setClientId(clientId);
entity.setUserSessionId(clientSession.getUserSession().getId());
entity.setUserId(clientSession.getUserSession().getId());
entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol());

View File

@ -78,7 +78,10 @@ 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.stream.ClientSessionFilterByUser;
import org.keycloak.models.sessions.infinispan.stream.MapEntryToKeyMapper;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.RemoveKeyConsumer;
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
@ -107,6 +110,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
protected final ClientSessionPersistentChangelogBasedTransaction clientSessionTx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
protected final UserSessionPersisterProvider userSessionPersister;
public PersistentUserSessionProvider(KeycloakSession session,
UserSessionPersistentChangelogBasedTransaction sessionTx,
@ -121,6 +125,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
userSessionPersister = session.getProvider(UserSessionPersisterProvider.class);
}
protected Cache<String, SessionEntityWrapper<UserSessionEntity>> getCache(boolean offline) {
@ -148,6 +153,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
entity.setRealmId(realm.getId());
entity.setClientId(client.getId());
entity.setUserSessionId(userSession.getId());
entity.setUserId(userSession.getUser().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()));
@ -392,7 +398,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
}
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));
}
@ -485,13 +490,9 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
}
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);
}
userSessionPersister.onUserRemoved(realm, user);
removeCachedUserAndClientSessionForUser(realm.getId(), user.getId(), true);
removeCachedUserAndClientSessionForUser(realm.getId(), user.getId(), false);
}
@Override
@ -640,6 +641,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
UserSessionEntity userSessionEntityToImport = createUserSessionEntityInstance(persistentUserSession);
String realmId = userSessionEntityToImport.getRealmId();
String sessionId = userSessionEntityToImport.getId();
String userId = userSessionEntityToImport.getUser();
RealmModel realm = session.realms().getRealm(realmId);
long lifespan = offline ?
@ -660,9 +662,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
for (Map.Entry<String, AuthenticatedClientSessionModel> entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) {
String clientUUID = entry.getKey();
AuthenticatedClientSessionModel clientSession = entry.getValue();
AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(sessionId, clientSession,
AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(sessionId, userId, clientSession,
realmId, clientUUID, offline);
clientSessionToImport.setUserSessionId(sessionId);
if (offline) {
// Update timestamp to the same value as userSession. LastSessionRefresh of userSession from DB will have a correct value.
@ -766,9 +767,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
private AuthenticatedClientSessionAdapter importOfflineClientSession(UserSessionAdapter<?> sessionToImportInto,
AuthenticatedClientSessionModel clientSession) {
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(sessionToImportInto.getId(), clientSession,
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(sessionToImportInto.getId(), sessionToImportInto.getUser().getId(), clientSession,
sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), true);
entity.setUserSessionId(sessionToImportInto.getId());
// Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value
entity.setTimestamp(sessionToImportInto.getLastSessionRefresh());
@ -795,9 +795,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
for (Map.Entry<String, AuthenticatedClientSessionModel> entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) {
String clientUUID = entry.getKey();
AuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(persistentUserSession.getId(), entry.getValue(),
AuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(persistentUserSession.getId(), userSessionEntity.getUser(), entry.getValue(),
userSessionEntity.getRealmId(), clientUUID, offline);
clientSession.setUserSessionId(userSessionEntity.getId());
if (offline) {
// Update timestamp to the same value as userSession. LastSessionRefresh of userSession from DB will have a correct value.
@ -918,6 +917,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
clientSession.getEntity().setClientId(clientId);
}
clientSession.getEntity().setUserSessionId(sessionEntityWrapper.getEntity().getId());
clientSession.getEntity().setUserId(sessionEntityWrapper.getEntity().getUser());
MergedUpdate<AuthenticatedClientSessionEntity> merged = MergedUpdate.computeUpdate(Collections.singletonList(Tasks.addIfAbsentSync()), clientSession, 1, 1);
clientSessionPerformer.registerChange(Map.entry(key, new SessionUpdatesList<>(realm, clientSession)), merged);
}
@ -956,4 +956,25 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
private void addClientSessionToUserSession(EmbeddedClientSessionKey cacheKey, boolean offline) {
sessionTx.registerClientSession(cacheKey.userSessionId(), cacheKey.clientId(), offline);
}
private void removeCachedUserAndClientSessionForUser(String realmId, String userId, boolean offline) {
if (getCache(offline) == null) {
// caching disabled
return;
}
try (var stream = getCache(offline).getAdvancedCache()
.entrySet()
.stream()
.filter(UserSessionPredicate.create(realmId).user(userId))
.map(MapEntryToKeyMapper.getInstance())) {
stream.forEach(RemoveKeyConsumer.getInstance());
}
try (var stream = getClientSessionCache(offline) .getAdvancedCache()
.entrySet()
.stream()
.filter(new ClientSessionFilterByUser(realmId, userId))
.map(MapEntryToKeyMapper.getInstance())) {
stream.forEach(RemoveKeyConsumer.getInstance());
}
}
}

View File

@ -136,7 +136,7 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
return authenticatedClientSessionEntitySessionEntityWrapper;
}
public static AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession,
public static AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, String userId, AuthenticatedClientSessionModel clientSession,
String realmId, String clientId, boolean offline) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
@ -151,12 +151,13 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
entity.setTimestamp(clientSession.getTimestamp());
entity.setOffline(offline);
entity.setUserSessionId(userSessionId);
entity.setUserId(userId);
return entity;
}
private SessionEntityWrapper<AuthenticatedClientSessionEntity> importClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionModel persistentClientSession, EmbeddedClientSessionKey clientSessionId) {
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), persistentClientSession,
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), userSession.getUser().getId(), persistentClientSession,
realm.getId(), client.getId(), userSession.isOffline());
boolean offline = userSession.isOffline();

View File

@ -25,7 +25,6 @@ import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;
import org.infinispan.protostream.annotations.ProtoReserved;
import org.infinispan.protostream.annotations.ProtoTypeId;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.marshalling.Marshalling;
import org.keycloak.models.AuthenticatedClientSessionModel;
@ -44,8 +43,6 @@ import org.keycloak.models.UserSessionModel;
)
public class AuthenticatedClientSessionEntity extends SessionEntity {
public static final Logger logger = Logger.getLogger(AuthenticatedClientSessionEntity.class);
// Metadata attribute, which contains the last timestamp available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
@Deprecated(since = "26.4", forRemoval = true)
public static final String LAST_TIMESTAMP_REMOTE = "lstr";
@ -62,6 +59,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
// TODO [pruivo] [KC27] make these fields final. They are the client session identity.
private volatile String userSessionId;
private volatile String clientId;
private volatile String userId;
public AuthenticatedClientSessionEntity() {
}
@ -117,7 +115,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.clientId = clientId;
}
@ProtoField(value = 5)
@ProtoField(5)
public String getAction() {
return action;
}
@ -135,6 +133,15 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.notes = notes;
}
@ProtoField(10)
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
@ -152,7 +159,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
// factory method required because of final fields
@ProtoFactory
AuthenticatedClientSessionEntity(String realmId, String authMethod, String redirectUri, int timestamp, String action, Map<String, String> notes, String userSessionId, String clientId) {
AuthenticatedClientSessionEntity(String realmId, String authMethod, String redirectUri, int timestamp, String action, Map<String, String> notes, String userSessionId, String clientId, String userId) {
super(realmId);
this.authMethod = authMethod;
this.redirectUri = redirectUri;
@ -161,6 +168,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.notes = notes;
this.userSessionId = userSessionId;
this.clientId = clientId;
this.userId = userId;
}
@ProtoField(8)
@ -182,6 +190,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
if (userSession.isRememberMe()) {
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true");
}
entity.setUserId(userSession.getUser().getId());
return entity;
}
@ -190,5 +199,4 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
entity.setNotes(model.getNotes() == null ? new ConcurrentHashMap<>() : model.getNotes());
return entity;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2025 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.stream;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import org.infinispan.protostream.annotations.Proto;
import org.infinispan.protostream.annotations.ProtoTypeId;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import static org.keycloak.marshalling.Marshalling.CLIENT_SESSION_USER_FILTER;
/**
* A {@link Predicate} to filter {@link AuthenticatedClientSessionEntity} values based on the Realm ID and the User ID.
*
* @param realmId The Realm ID.
* @param userId The User ID.
*/
@ProtoTypeId(CLIENT_SESSION_USER_FILTER)
@Proto
public record ClientSessionFilterByUser(String realmId,
String userId) implements Predicate<Map.Entry<?, SessionEntityWrapper<AuthenticatedClientSessionEntity>>> {
@Override
public boolean test(Map.Entry<?, SessionEntityWrapper<AuthenticatedClientSessionEntity>> entry) {
var entity = entry.getValue().getEntity();
return Objects.equals(userId, entity.getUserId()) &&
Objects.equals(realmId, entity.getRealmId());
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2025 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.stream;
import java.util.function.BiConsumer;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoTypeId;
import static org.keycloak.marshalling.Marshalling.REMOVE_KEY_BI_CONSUMER;
/**
* Removes keys from a {@link Cache}.
* <p>
* This implementation is best-effortly, meaning if the removal fails, it won't throw any exception.
*
* @param <K> The type of key stored in the cache.
* @param <V> The type of the value store in the cache.
*/
@ProtoTypeId(REMOVE_KEY_BI_CONSUMER)
public class RemoveKeyConsumer<K, V> implements BiConsumer<Cache<K, V>, K> {
private static final RemoveKeyConsumer<Object, Object> INSTANCE = new RemoveKeyConsumer<>();
@ProtoFactory
@SuppressWarnings("unchecked")
public static <K, V> RemoveKeyConsumer<K, V> getInstance() {
return (RemoveKeyConsumer<K, V>) INSTANCE;
}
@Override
public void accept(Cache<K, V> cache, K key) {
cache.getAdvancedCache()
.withFlags(Flag.ZERO_LOCK_ACQUISITION_TIMEOUT, Flag.FAIL_SILENTLY, Flag.IGNORE_RETURN_VALUES)
.remove(key);
}
}

View File

@ -25,8 +25,14 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.hamcrest.Matchers;
@ -73,6 +79,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -746,6 +753,113 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
});
}
@Test
public void testUserRemoved() throws InterruptedException {
final String userName = "to-remove";
final int numberOfSessions = 5;
final int clusterSize = 4;
inComittedTransaction(session -> {
RealmModel realm = getRealm(session);
session.sessions().removeUserSessions(realm);
session.users().addUser(realm, userName).setEmail(userName + "@localhost");
});
final UserSessionCount initial = getUserSessionCount();
final CyclicBarrier barrier = new CyclicBarrier(clusterSize);
final AtomicBoolean userDeleted = new AtomicBoolean(false);
inIndependentFactories(clusterSize, 60, () -> {
try {
barrier.await(10, TimeUnit.SECONDS);
inComittedTransaction(session -> {
RealmModel realm = getRealm(session);
UserModel user = session.users().getUserByUsername(realm, userName);
ClientModel testApp = realm.getClientByClientId("test-app");
IntStream.range(0, numberOfSessions)
.forEach(ignored -> {
UserSessionModel us = session.sessions().createUserSession(null, realm, user, userName, "127.0.0.1", "form", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
session.sessions().createClientSession(realm, testApp, us);
});
});
barrier.await(10, TimeUnit.SECONDS);
assertSessionCount(numberOfSessions * clusterSize, initial);
barrier.await(10, TimeUnit.SECONDS);
if (userDeleted.compareAndSet(false, true)) {
inComittedTransaction(session -> {
RealmModel realm = getRealm(session);
UserModel user = session.users().getUserByUsername(realm, userName);
new UserManager(session).removeUser(realm, user);
});
}
barrier.await(10, TimeUnit.SECONDS);
assertSessionCount(0, initial);
barrier.await(10, TimeUnit.SECONDS);
} catch (BrokenBarrierException | TimeoutException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
private UserSessionCount getUserSessionCount() {
if (InfinispanUtils.isEmbeddedInfinispan()) {
return MultiSiteUtils.isPersistentSessionsEnabled() ?
new UserSessionCount(getPersistedUserSessionsCount(), getEmbeddedCachedUserSessionsCount()) :
new UserSessionCount(-1, getEmbeddedCachedUserSessionsCount());
}
return MultiSiteUtils.isPersistentSessionsEnabled() ?
new UserSessionCount(getPersistedUserSessionsCount(), -1) :
new UserSessionCount(-1, getRemoteCachedUserSessionsCount());
}
private void assertSessionCount(int offset, UserSessionCount initial) {
UserSessionCount current = getUserSessionCount();
if (initial.database() != -1) {
assertEquals("Wrong number of session in database", initial.database() + offset, current.database());
} else {
assertEquals("Wrong number of session in database", initial.database(), current.database());
}
if (initial.cache() != -1) {
assertEquals("Wrong number of session in cache", initial.cache() + offset, current.cache());
} else {
assertEquals("Wrong number of session in cache", initial.cache(), current.cache());
}
}
private int getRemoteCachedUserSessionsCount() {
return inComittedTransaction(session -> {
getRealm(session);
return session.getProvider(InfinispanConnectionProvider.class).getRemoteCache(USER_SESSION_CACHE_NAME).size();
});
}
private int getEmbeddedCachedUserSessionsCount() {
return inComittedTransaction(session -> {
getRealm(session);
return session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size();
});
}
private int getPersistedUserSessionsCount() {
return inComittedTransaction(session -> {
getRealm(session);
return session.getProvider(UserSessionPersisterProvider.class).getUserSessionsCount(false);
});
}
private RealmModel getRealm(KeycloakSession session) {
RealmModel realm = session.realms().getRealm(realmId);
session.getContext().setRealm(realm);
return realm;
}
private long countUserSessionsInRealm(KeycloakSession session) {
JpaUserSessionPersisterProvider sessionPersisterProvider = (JpaUserSessionPersisterProvider) session.getProvider(UserSessionPersisterProvider.class);
RealmModel realm = session.realms().getRealm(realmId);
@ -886,4 +1000,6 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
assertThat(actual, Matchers.arrayContainingInAnyOrder(expectedSessionIds));
}
private record UserSessionCount(int database, int cache) {}
}