mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
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:
parent
f9fd9bce9e
commit
39964befef
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user