Reuse already fixed code to fetch offline user (#22429)

The problem is again the wrap(...) function.
In case the user is not found, then null is
returned. This can happen when a federated user
is deleted on the federation side but Keycloak
is not informed about it. In that case, the
session is still present but no UserModel can
be created.

Without this patch the stream contains null
values. Some downstream users can not cope well
with that.

The adjustment of the function getUserSessionsCount(...)
is slightly more expensive in execution, but
returns the correct number.

Closes #22428

Co-authored-by: Martin Krüger <mkrueger@mkru.de>
This commit is contained in:
mkrueger92 2023-09-21 22:19:09 +02:00 committed by GitHub
parent 47d9ae71c4
commit 498be3d928
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -65,6 +65,7 @@ import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
@ -331,9 +332,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, boolean offline) { private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, boolean offline) {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline); InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
SessionEntityWrapper<UserSessionEntity> entityWrapper = tx.get(id); SessionEntityWrapper<UserSessionEntity> entityWrapper = tx.get(id);
if (entityWrapper==null) return null; if (entityWrapper == null) {
return null;
}
UserSessionEntity entity = entityWrapper.getEntity(); UserSessionEntity entity = entityWrapper.getEntity();
if (!entity.getRealmId().equals(realm.getId())) return null; if (!entity.getRealmId().equals(realm.getId())) {
return null;
}
return entity; return entity;
} }
@ -472,23 +477,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return persister.loadUserSessionsStream(realm, client, true, firstResult, maxResults); return persister.loadUserSessionsStream(realm, client, true, firstResult, maxResults);
} }
final String clientUuid = client.getId(); UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(client.getId());
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(clientUuid);
return getUserSessionModels(realm, firstResult, maxResults, offline, predicate); return paginatedStream(getUserSessionsStream(realm, predicate, offline)
} .sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh)), firstResult, maxResults);
protected Stream<UserSessionModel> getUserSessionModels(RealmModel realm, Integer firstResult, Integer maxResults, boolean offline, UserSessionPredicate predicate) {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
// return a stream that 'wraps' the infinispan cache stream so that the cache stream's elements are read one by one
// and then filtered/mapped locally to avoid serialization issues when trying to manipulate the cache stream directly.
Stream<UserSessionEntity> stream = StreamSupport.stream(cache.entrySet().stream().filter(predicate).spliterator(), false)
.map(Mappers.userSessionEntity())
.sorted(Comparators.userSessionLastSessionRefresh());
return paginatedStream(stream, firstResult, maxResults).map(entity -> this.wrap(realm, entity, offline));
} }
@Override @Override
@ -571,7 +563,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return cache.entrySet().stream() return cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId())) .filter(UserSessionPredicate.create(realm.getId()))
.map(Mappers.authClientSessionSetMapper()) .map(Mappers.authClientSessionSetMapper())
.flatMap((Serializable & Function<Set<String>, Stream<? extends String>>)Mappers::toStream) .flatMap((Serializable & Function<Set<String>, Stream<? extends String>>) Mappers::toStream)
.collect( .collect(
CacheCollectors.serializableCollector( CacheCollectors.serializableCollector(
() -> Collectors.groupingBy(Function.identity(), Collectors.counting()) () -> Collectors.groupingBy(Function.identity(), Collectors.counting())
@ -579,7 +571,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
); );
} }
protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
if (offline && loadOfflineSessionsFromDatabase) { if (offline && loadOfflineSessionsFromDatabase) {
// fetch the actual offline user session count from the database // fetch the actual offline user session count from the database
@ -587,14 +579,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return persister.getUserSessionsCount(realm, client, true); return persister.getUserSessionsCount(realm, client, true);
} }
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline); return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).client(client.getId()), offline).count();
cache = CacheDecorators.skipCacheLoaders(cache);
final String clientUuid = client.getId();
return cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(clientUuid))
.count();
} }
@Override @Override
@ -759,7 +744,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return null; return null;
} }
UserModel user = session.users().getUserById(realm, entity.getUser()); UserModel user = session.users().getUserById(realm, entity.getUser());
if (user == null) { if (user == null) {
return null; return null;
} }
@ -770,12 +755,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) { AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline); InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline); InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, clientSessionUpdateTx, offline) : null; return entity != null ? new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, clientSessionUpdateTx, offline) : null;
} }
UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) { UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) {
if (userSession instanceof UserSessionAdapter) { if (userSession instanceof UserSessionAdapter) {
if (!userSession.getRealm().equals(realm)) return null; if (!userSession.getRealm().equals(realm)) {
return null;
}
return ((UserSessionAdapter) userSession).getEntity(); return ((UserSessionAdapter) userSession).getEntity();
} else { } else {
return getUserSessionEntity(realm, userSession.getId(), offline); return getUserSessionEntity(realm, userSession.getId(), offline);
@ -942,7 +929,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
// Import client sessions // Import client sessions
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessCache = Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessCache =
CacheDecorators.skipCacheLoaders(offline ? offlineClientSessionCache : clientSessionCache); CacheDecorators.skipCacheLoaders(offline ? offlineClientSessionCache : clientSessionCache);
if (importWithExpiration) { if (importWithExpiration) {
importSessionsWithExpiration(clientSessionsById, clientSessCache, importSessionsWithExpiration(clientSessionsById, clientSessCache,
@ -986,8 +973,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
private <T extends SessionEntity> void importSessionsWithExpiration(Map<? extends Object, SessionEntityWrapper<T>> sessionsById, private <T extends SessionEntity> void importSessionsWithExpiration(Map<? extends Object, SessionEntityWrapper<T>> sessionsById,
BasicCache cache, SessionFunction<T> lifespanMsCalculator, BasicCache cache, SessionFunction<T> lifespanMsCalculator,
SessionFunction<T> maxIdleTimeMsCalculator) { SessionFunction<T> maxIdleTimeMsCalculator) {
sessionsById.forEach((id, sessionEntityWrapper) -> { sessionsById.forEach((id, sessionEntityWrapper) -> {
T sessionEntity = sessionEntityWrapper.getEntity(); T sessionEntity = sessionEntityWrapper.getEntity();
@ -996,8 +983,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
long lifespan = lifespanMsCalculator.apply(currentRealm, client, sessionEntity); long lifespan = lifespanMsCalculator.apply(currentRealm, client, sessionEntity);
long maxIdle = maxIdleTimeMsCalculator.apply(currentRealm, client, sessionEntity); long maxIdle = maxIdleTimeMsCalculator.apply(currentRealm, client, sessionEntity);
if(lifespan != SessionTimeouts.ENTRY_EXPIRED_FLAG if (lifespan != SessionTimeouts.ENTRY_EXPIRED_FLAG
&& maxIdle != SessionTimeouts.ENTRY_EXPIRED_FLAG ) { && maxIdle != SessionTimeouts.ENTRY_EXPIRED_FLAG) {
if (cache instanceof RemoteCache) { if (cache instanceof RemoteCache) {
Retry.executeWithBackoff((int iteration) -> { Retry.executeWithBackoff((int iteration) -> {
@ -1052,7 +1039,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (userSession instanceof OfflineUserSessionModel) { if (userSession instanceof OfflineUserSessionModel) {
// this is a hack so that UserModel doesn't have to be available when offline token is imported. // 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 // see related JIRA - KEYCLOAK-5350 and corresponding test
OfflineUserSessionModel oline = (OfflineUserSessionModel)userSession; OfflineUserSessionModel oline = (OfflineUserSessionModel) userSession;
entity.setUser(oline.getUserId()); entity.setUser(oline.getUserId());
// NOTE: Hack // NOTE: Hack
// We skip calling entity.setLoginUsername(userSession.getLoginUsername()) // We skip calling entity.setLoginUsername(userSession.getLoginUsername())
@ -1098,12 +1085,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId); SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask); userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, offline); return new AuthenticatedClientSessionAdapter(session, this, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, offline);
} }
private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession, private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession,
String realmId, String clientId, boolean offline) { String realmId, String clientId, boolean offline) {
final UUID clientSessionId = keyGenerator.generateKeyUUID(session, getClientSessionCache(offline)); final UUID clientSessionId = keyGenerator.generateKeyUUID(session, getClientSessionCache(offline));
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId);
entity.setRealmId(realmId); entity.setRealmId(realmId);