diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java index 9a4065c8f4..83d5f1209b 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java @@ -94,7 +94,7 @@ public class OfflinePersistentUserSessionLoader implements SessionLoader sessions = persister diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/PaginationUtils.java b/model/jpa/src/main/java/org/keycloak/models/jpa/PaginationUtils.java index f52014acf1..cdb64906b0 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/PaginationUtils.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/PaginationUtils.java @@ -22,11 +22,11 @@ import jakarta.persistence.TypedQuery; public class PaginationUtils { public static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE >> 1; - + public static TypedQuery paginateQuery(TypedQuery query, Integer first, Integer max) { - if (first != null && first > 0) { + if (first != null && first >= 0) { query = query.setFirstResult(first); - + // Workaround for https://hibernate.atlassian.net/browse/HHH-14295 if (max == null || max < 0) { max = DEFAULT_MAX_RESULTS; 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 2d5b3745af..935863f971 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 @@ -417,7 +417,6 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv * @return */ private Stream loadUserSessionsWithClientSessions(TypedQuery query, String offlineStr, boolean useExact) { - List userSessionAdapters = closing(query.getResultStream() .map(this::toAdapter) .filter(Objects::nonNull)) @@ -463,6 +462,8 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv onClientRemoved(clientUUID); } + logger.tracef("Loaded %d user sessions (offline=%s, sessionIds=%s)", userSessionAdapters.size(), offlineStr, sessionsById.keySet()); + return userSessionAdapters.stream().map(UserSessionModel.class::cast); } @@ -471,9 +472,12 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv PersistentAuthenticatedClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSessionEntity); if (clientSessAdapter.getClient() == null) { + logger.tracef("Not adding client session %s / %s since client is null", userSession, clientSessAdapter); return false; } + logger.tracef("Adding client session %s / %s", userSession, clientSessAdapter); + String clientId = clientSessionEntity.getClientId(); if (isExternalClient(clientSessionEntity)) { clientId = getExternalClientId(clientSessionEntity); diff --git a/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java b/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java index 362038fd3b..00a7155127 100644 --- a/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java +++ b/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java @@ -233,6 +233,11 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate return getId().hashCode(); } + @Override + public String toString() { + return getId(); + } + protected static class PersistentClientSessionData { @JsonProperty("authMethod") diff --git a/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index e1607ead2e..da5cb96bd6 100644 --- a/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java +++ b/model/legacy-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -257,6 +257,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel { return getId().hashCode(); } + @Override + public String toString() { + return getId(); + } + protected static class PersistentUserSessionData { @JsonProperty("brokerSessionId") 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 debd012df5..dbeaf7fbc5 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 @@ -43,12 +43,15 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Random; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -57,6 +60,7 @@ import org.junit.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; /** * @@ -345,16 +349,25 @@ public class OfflineSessionPersistenceTest extends KeycloakModelTest { @RequireProvider(UserSessionPersisterProvider.class) @RequireProvider(value = UserSessionProvider.class, only = InfinispanUserSessionProviderFactory.PROVIDER_ID) public void testLazyOfflineUserSessionFetching() { - List offlineSessionIds = createOfflineSessions(realmId, userIds); + Map> offlineSessionIdsDetailed = createOfflineSessionsDetailed(realmId, userIds); + Collection offlineSessionIds = offlineSessionIdsDetailed.values().stream().flatMap(Set::stream).collect(Collectors.toCollection(TreeSet::new)); assertOfflineSessionsExist(realmId, offlineSessionIds); // Simulate server restart reinitializeKeycloakSessionFactory(); - List actualOfflineSessionIds = withRealm(realmId, (session, realm) -> session.users().getUsersStream(realm).flatMap(user -> - session.sessions().getOfflineUserSessionsStream(realm, user)).map(UserSessionModel::getId).collect(Collectors.toList())); + Map> actualOfflineSessionIds = withRealm(realmId, (session, realm) -> session.users() + .getUsersStream(realm) + .collect(Collectors.toMap( + UserModel::getId, + user -> session.sessions().getOfflineUserSessionsStream(realm, user).map(UserSessionModel::getId).collect(Collectors.toCollection(TreeSet::new)) + )) + ); - assertThat(actualOfflineSessionIds, containsInAnyOrder(offlineSessionIds.toArray())); + assertThat("User IDs", actualOfflineSessionIds.keySet(), equalTo(offlineSessionIdsDetailed.keySet())); + for (Entry> me : offlineSessionIdsDetailed.entrySet()) { + assertThat("Session IDs", actualOfflineSessionIds.get(me.getKey()), equalTo(me.getValue())); + } } private String createOfflineClientSession(String offlineUserSessionId, String clientId) { @@ -408,6 +421,16 @@ public class OfflineSessionPersistenceTest extends KeycloakModelTest { ); } + private Map> createOfflineSessionsDetailed(String realmId, List userIds) { + return withRealm(realmId, (session, realm) -> + userIds.stream() + .collect(Collectors.toMap( + Function.identity(), + userId -> createOfflineSessions(session, realm, userId, us -> {}).map(UserSessionModel::getId).collect(Collectors.toCollection(TreeSet::new)) + )) + ); + } + /** * Creates {@link #OFFLINE_SESSION_COUNT_PER_USER} offline sessions for {@code userId} user. */