Persistent sessions code also for offline sessions (#28319)

Persistent sessions code also for offline sessions

Closes #28318

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-04-12 13:15:02 +02:00 committed by GitHub
parent fd97072a62
commit b4cfebd8d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 131 additions and 141 deletions

View file

@ -309,6 +309,10 @@ jobs:
if: needs.conditional.outputs.ci-store == 'true' if: needs.conditional.outputs.ci-store == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 150 timeout-minutes: 150
strategy:
matrix:
variant: ["persistent-user-sessions,persistent-user-sessions-no-cache", "persistent-user-sessions"]
fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -316,11 +320,11 @@ jobs:
name: Integration test setup name: Integration test setup
uses: ./.github/actions/integration-test-setup uses: ./.github/actions/integration-test-setup
- name: Run base tests - name: Run base tests without cache
run: | run: |
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh persistent-sessions` TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh persistent-sessions`
echo "Tests: $TESTS" 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 ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dauth.server.features=${{ matrix.variant }} -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
- name: Upload JVM Heapdumps - name: Upload JVM Heapdumps
if: always() if: always()

View file

@ -114,9 +114,9 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
protected final RemoteCacheInvoker remoteCacheInvoker; protected final RemoteCacheInvoker remoteCacheInvoker;
protected final InfinispanKeyGenerator keyGenerator; protected final InfinispanKeyGenerator keyGenerator;
protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster; protected final SessionFunction<UserSessionEntity> offlineSessionCacheEntryLifespanAdjuster;
protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster; protected final SessionFunction<AuthenticatedClientSessionEntity> offlineClientSessionCacheEntryLifespanAdjuster;
public PersistentUserSessionProvider(KeycloakSession session, public PersistentUserSessionProvider(KeycloakSession session,
RemoteCacheInvoker remoteCacheInvoker, RemoteCacheInvoker remoteCacheInvoker,
@ -206,12 +206,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
@Override @Override
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
final UUID clientSessionId; final UUID clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSession.getId(), client.getId());
if (userSession.isOffline()) {
clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache);
} else {
clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSession.getId(), client.getId());
}
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId);
entity.setRealmId(realm.getId()); entity.setRealmId(realm.getId());
entity.setClientId(client.getId()); entity.setClientId(client.getId());
@ -244,7 +239,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync(); SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState); clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState);
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId); SessionUpdateTask<UserSessionEntity> registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId);
userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask); userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask);
return adapter; return adapter;
@ -797,18 +792,17 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
return getUserSessionsStream(realm, client, first, max, true); return getUserSessionsStream(realm, client, first, max, true);
} }
@Override @Override
public void importUserSessions(Collection<UserSessionModel> persistentUserSessions, boolean offline) { public void importUserSessions(Collection<UserSessionModel> persistentUserSessions, boolean offline) {
if (persistentUserSessions == null || persistentUserSessions.isEmpty()) { if (persistentUserSessions == null || persistentUserSessions.isEmpty()) {
return; return;
} }
persistentUserSessions.forEach(userSessionModel -> importUserSession(userSessionModel, offline));
}
public SessionEntityWrapper<UserSessionEntity> importUserSession(UserSessionModel persistentUserSession, boolean offline) {
Map<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsById = new HashMap<>(); Map<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsById = new HashMap<>();
Map<String, SessionEntityWrapper<UserSessionEntity>> sessionsById = persistentUserSessions.stream()
.map((UserSessionModel persistentUserSession) -> {
UserSessionEntity userSessionEntityToImport = createUserSessionEntityInstance(persistentUserSession); UserSessionEntity userSessionEntityToImport = createUserSessionEntityInstance(persistentUserSession);
for (Map.Entry<String, AuthenticatedClientSessionModel> entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { for (Map.Entry<String, AuthenticatedClientSessionModel> entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) {
@ -828,68 +822,41 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
clientSessions.put(clientUUID, clientSessionToImport.getId()); clientSessions.put(clientUUID, clientSessionToImport.getId());
} }
return userSessionEntityToImport; SessionEntityWrapper<UserSessionEntity> wrappedUserSessionEntity = new SessionEntityWrapper<>(userSessionEntityToImport);
})
.map(SessionEntityWrapper::new) Map<String, SessionEntityWrapper<UserSessionEntity>> sessionsById =
.collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity())); Stream.of(wrappedUserSessionEntity).collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity()));
// Directly put all entities to the infinispan cache // Directly put all entities to the infinispan cache
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(getCache(offline)); Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(getCache(offline));
boolean importWithExpiration = sessionsById.size() == 1; sessionsById = importSessionsWithExpiration(sessionsById, cache,
if (importWithExpiration) {
importSessionsWithExpiration(sessionsById, cache,
offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs,
offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs);
} else {
Retry.executeWithBackoff((int iteration) -> { if (sessionsById.isEmpty()) {
cache.putAll(sessionsById); return null;
}, 10, 10);
} }
// put all entities to the remoteCache (if exists) // put all entities to the remoteCache (if exists)
RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
if (remoteCache != null) { if (remoteCache != null) {
Map<String, SessionEntityWrapper<UserSessionEntity>> sessionsByIdForTransport = sessionsById.values().stream() Map<String, SessionEntityWrapper<UserSessionEntity>> sessionsByIdForTransport = Stream.of(wrappedUserSessionEntity)
.map(SessionEntityWrapper::forTransport) .map(SessionEntityWrapper::forTransport)
.collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity())); .collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity()));
if (importWithExpiration) {
importSessionsWithExpiration(sessionsByIdForTransport, remoteCache, importSessionsWithExpiration(sessionsByIdForTransport, remoteCache,
offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs,
offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); 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 // Import client sessions
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessCache = Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessCache =
CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(offline ? offlineClientSessionCache : clientSessionCache); CacheDecorators.skipCacheLoadersIfRemoteStoreIsEnabled(offline ? offlineClientSessionCache : clientSessionCache);
if (importWithExpiration) {
importSessionsWithExpiration(clientSessionsById, clientSessCache, importSessionsWithExpiration(clientSessionsById, clientSessCache,
offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs,
offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs);
} else {
Retry.executeWithBackoff((int iteration) -> {
clientSessCache.putAll(clientSessionsById);
}, 10, 10);
}
// put all entities to the remoteCache (if exists) // put all entities to the remoteCache (if exists)
RemoteCache remoteCacheClientSessions = InfinispanUtil.getRemoteCache(clientSessCache); RemoteCache remoteCacheClientSessions = InfinispanUtil.getRemoteCache(clientSessCache);
@ -898,38 +865,22 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
.map(SessionEntityWrapper::forTransport) .map(SessionEntityWrapper::forTransport)
.collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity())); .collect(Collectors.toMap(sessionEntityWrapper -> sessionEntityWrapper.getEntity().getId(), Function.identity()));
if (importWithExpiration) {
importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions, importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions,
offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs,
offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); 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. return sessionsById.entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null);
throw re;
} }
}, 10, 10); private <T extends SessionEntity, K> Map<K, SessionEntityWrapper<T>> importSessionsWithExpiration(Map<K, SessionEntityWrapper<T>> sessionsById,
} BasicCache<K, SessionEntityWrapper<T>> cache, SessionFunction<T> lifespanMsCalculator,
}
}
private <T extends SessionEntity> void importSessionsWithExpiration(Map<? extends Object, SessionEntityWrapper<T>> sessionsById,
BasicCache cache, SessionFunction<T> lifespanMsCalculator,
SessionFunction<T> maxIdleTimeMsCalculator) { SessionFunction<T> maxIdleTimeMsCalculator) {
sessionsById.forEach((id, sessionEntityWrapper) -> { return sessionsById.entrySet().stream().map(entry -> {
T sessionEntity = sessionEntityWrapper.getEntity(); T sessionEntity = entry.getValue().getEntity();
RealmModel currentRealm = session.realms().getRealm(sessionEntity.getRealmId()); RealmModel currentRealm = session.realms().getRealm(sessionEntity.getRealmId());
ClientModel client = sessionEntityWrapper.getClientIfNeeded(currentRealm); ClientModel client = entry.getValue().getClientIfNeeded(currentRealm);
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);
@ -939,7 +890,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
Retry.executeWithBackoff((int iteration) -> { Retry.executeWithBackoff((int iteration) -> {
try { try {
cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); cache.putIfAbsent(entry.getKey(), entry.getValue(), lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) { } catch (HotRodClientException re) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debugf(re, "Failed to put import %d sessions to remoteCache. Iteration '%s'. Will try to retry the task", log.debugf(re, "Failed to put import %d sessions to remoteCache. Iteration '%s'. Will try to retry the task",
@ -952,10 +903,13 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
}, 10, 10); }, 10, 10);
} else { } else {
cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); cache.putIfAbsent(entry.getKey(), entry.getValue(), lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
} }
return entry;
} else {
return null;
} }
}); }).filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
private UserSessionEntity createUserSessionEntityInstance(UserSessionModel userSession) { private UserSessionEntity createUserSessionEntityInstance(UserSessionModel userSession) {
@ -1021,7 +975,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions();
clientSessions.put(clientSession.getClient().getId(), clientSessionId); clientSessions.put(clientSession.getClient().getId(), clientSessionId);
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId); SessionUpdateTask<UserSessionEntity> 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);
@ -1030,12 +984,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession, private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession,
String realmId, String clientId, boolean offline) { String realmId, String clientId, boolean offline) {
final UUID clientSessionId; final UUID clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSessionId, clientId);
if (offline) {
clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache);
} else {
clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSessionId, clientId);
}
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId);
entity.setRealmId(realmId); entity.setRealmId(realmId);
@ -1084,7 +1033,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
clientSessionUpdateTx.addTask(clientSession.getId(), null, clientSession, UserSessionModel.SessionPersistenceState.PERSISTENT); clientSessionUpdateTx.addTask(clientSession.getId(), null, clientSession, UserSessionModel.SessionPersistenceState.PERSISTENT);
} }
return sessionTx.get(realm, userSessionEntity.getId()); return userSessionUpdateTx.get(userSessionEntity.getId());
} }
@ -1128,6 +1077,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
} }
public static UUID createClientSessionUUID(String userSessionId, String clientId) { public static UUID createClientSessionUUID(String userSessionId, String clientId) {
// This allows creating a UUID that is constant even if the entry is reloaded from the database
return UUID.nameUUIDFromBytes((userSessionId + clientId).getBytes(StandardCharsets.UTF_8)); return UUID.nameUUIDFromBytes((userSessionId + clientId).getBytes(StandardCharsets.UTF_8));
} }
} }

View file

@ -104,16 +104,7 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession, private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession,
String realmId, String clientId) { String realmId, String clientId) {
UUID clientSessionId = null; UUID clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSessionId, clientId);
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); AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId);
entity.setRealmId(realmId); entity.setRealmId(realmId);

View file

@ -38,6 +38,8 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.connections.infinispan.InfinispanUtil; import org.keycloak.connections.infinispan.InfinispanUtil;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
/** /**
@ -73,7 +75,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
public void addTask(K key, SessionUpdateTask<V> task) { public void addTask(K key, SessionUpdateTask<V> task) {
SessionUpdatesList<V> myUpdates = updates.get(key); SessionUpdatesList<V> myUpdates = updates.get(key);
if (myUpdates == null) { 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))) { if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && (Objects.equals(cacheName, USER_SESSION_CACHE_NAME) || Objects.equals(cacheName, CLIENT_SESSION_CACHE_NAME) || Objects.equals(cacheName, OFFLINE_USER_SESSION_CACHE_NAME) || Objects.equals(cacheName, OFFLINE_CLIENT_SESSION_CACHE_NAME))) {
throw new IllegalStateException("Can't load from cache"); throw new IllegalStateException("Can't load from cache");
} }

View file

@ -47,6 +47,11 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionChangesPerformer<K, V> { public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionChangesPerformer<K, V> {
private final KeycloakSession session; private final KeycloakSession session;
@ -69,8 +74,8 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
private TriConsumer<KeycloakSession, Map.Entry<K, SessionUpdatesList<V>>, MergedUpdate<V>> processor() { private TriConsumer<KeycloakSession, Map.Entry<K, SessionUpdatesList<V>>, MergedUpdate<V>> processor() {
return switch (cacheName) { return switch (cacheName) {
case "sessions", "offlineSessions" -> this::processUserSessionUpdate; case USER_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME -> this::processUserSessionUpdate;
case "clientSessions", "offlineClientSessions" -> this::processClientSessionUpdate; case CLIENT_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME -> this::processClientSessionUpdate;
default -> throw new IllegalStateException("Unexpected value: " + cacheName); default -> throw new IllegalStateException("Unexpected value: " + cacheName);
}; };
} }

View file

@ -30,6 +30,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
public class PersistentSessionsChangelogBasedTransaction<K, V extends SessionEntity> extends InfinispanChangelogBasedTransaction<K, V> { public class PersistentSessionsChangelogBasedTransaction<K, V extends SessionEntity> extends InfinispanChangelogBasedTransaction<K, V> {
@ -45,7 +47,8 @@ public class PersistentSessionsChangelogBasedTransaction<K, V extends SessionEnt
throw new IllegalStateException("Persistent user sessions are not enabled"); 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))) { if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) &&
(cache.getName().equals(USER_SESSION_CACHE_NAME) || cache.getName().equals(CLIENT_SESSION_CACHE_NAME) || cache.getName().equals(OFFLINE_USER_SESSION_CACHE_NAME) || cache.getName().equals(OFFLINE_CLIENT_SESSION_CACHE_NAME))) {
changesPerformers = List.of( changesPerformers = List.of(
new JpaChangesPerformer<>(session, cache.getName(), offline) new JpaChangesPerformer<>(session, cache.getName(), offline)
); );

View file

@ -35,6 +35,8 @@ import java.util.Collections;
import java.util.Objects; import java.util.Objects;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
public class UserSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction<String, UserSessionEntity> { public class UserSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction<String, UserSessionEntity> {
@ -48,7 +50,7 @@ public class UserSessionPersistentChangelogBasedTransaction extends PersistentSe
SessionUpdatesList<UserSessionEntity> myUpdates = updates.get(key); SessionUpdatesList<UserSessionEntity> myUpdates = updates.get(key);
if (myUpdates == null) { if (myUpdates == null) {
SessionEntityWrapper<UserSessionEntity> wrappedEntity = null; SessionEntityWrapper<UserSessionEntity> 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))) { if (!((Objects.equals(cache.getName(), USER_SESSION_CACHE_NAME) || Objects.equals(cache.getName(), CLIENT_SESSION_CACHE_NAME) || Objects.equals(cache.getName(), OFFLINE_USER_SESSION_CACHE_NAME) || Objects.equals(cache.getName(), OFFLINE_CLIENT_SESSION_CACHE_NAME)) && Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE))) {
wrappedEntity = cache.get(key); wrappedEntity = cache.get(key);
} }
if (wrappedEntity == null) { if (wrappedEntity == null) {
@ -111,15 +113,12 @@ public class UserSessionPersistentChangelogBasedTransaction extends PersistentSe
return null; 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))) { if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && (cache.getName().equals(USER_SESSION_CACHE_NAME) || cache.getName().equals(CLIENT_SESSION_CACHE_NAME) || cache.getName().equals(OFFLINE_USER_SESSION_CACHE_NAME) || cache.getName().equals(OFFLINE_CLIENT_SESSION_CACHE_NAME))) {
return ((PersistentUserSessionProvider) kcSession.getProvider(UserSessionProvider.class)).wrapPersistentEntity(persistentUserSession.getRealm(), offline, persistentUserSession); 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); LOG.debugf("Attempting to import user-session for sessionId=%s offline=%s", sessionId, offline);
kcSession.sessions().importUserSessions(Collections.singleton(persistentUserSession), offline); SessionEntityWrapper<UserSessionEntity> ispnUserSessionEntity = ((PersistentUserSessionProvider) kcSession.getProvider(UserSessionProvider.class)).importUserSession(persistentUserSession, offline);;
LOG.debugf("user-session imported, trying another lookup for sessionId=%s offline=%s", sessionId, offline);
SessionEntityWrapper<UserSessionEntity> ispnUserSessionEntity = cache.get(sessionId);
if (ispnUserSessionEntity != null) { if (ispnUserSessionEntity != null) {
LOG.debugf("user-session found after import for sessionId=%s offline=%s", sessionId, offline); LOG.debugf("user-session found after import for sessionId=%s offline=%s", sessionId, offline);

View file

@ -17,7 +17,7 @@
--> -->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="25.0.0-xxx"> <changeSet author="keycloak" id="25.0.0-28265-tables">
<addColumn tableName="OFFLINE_USER_SESSION"> <addColumn tableName="OFFLINE_USER_SESSION">
<!-- length(broker_session_id) + length(realm_id) <= 1700 for mssql --> <!-- length(broker_session_id) + length(realm_id) <= 1700 for mssql -->
<column name="BROKER_SESSION_ID" type="VARCHAR(1024)" /> <column name="BROKER_SESSION_ID" type="VARCHAR(1024)" />
@ -26,6 +26,13 @@
<addColumn tableName="OFFLINE_CLIENT_SESSION"> <addColumn tableName="OFFLINE_CLIENT_SESSION">
<column name="VERSION" type="INT" defaultValueNumeric="0" /> <column name="VERSION" type="INT" defaultValueNumeric="0" />
</addColumn> </addColumn>
<modifySql dbms="mssql">
<!-- ensure that existing rows also get the new values on mssql -->
<!-- https://github.com/liquibase/liquibase/issues/4644 -->
<replace replace="DEFAULT 0" with="DEFAULT 0 WITH VALUES" />
</modifySql>
</changeSet>
<changeSet author="keycloak" id="25.0.0-28265-index-creation">
<createIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_LAST_SESSION_REFRESH" > <createIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_LAST_SESSION_REFRESH" >
<!-- optimize this index for range queries for expire sessions --> <!-- optimize this index for range queries for expire sessions -->
<!-- it should also distribute hot segments across realms and online/offline --> <!-- it should also distribute hot segments across realms and online/offline -->
@ -33,17 +40,14 @@
<column name="OFFLINE_FLAG" /> <column name="OFFLINE_FLAG" />
<column name="LAST_SESSION_REFRESH" /> <column name="LAST_SESSION_REFRESH" />
</createIndex> </createIndex>
</changeSet>
<changeSet author="keycloak" id="25.0.0-28265-index-cleanup">
<dropIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_CREATEDON" /> <dropIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_CREATEDON" />
<dropIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_PRELOAD" /> <dropIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_PRELOAD" />
<dropIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_USERSESS" /> <dropIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_USERSESS" />
<dropIndex tableName="OFFLINE_CLIENT_SESSION" indexName="IDX_OFFLINE_CSS_PRELOAD" /> <dropIndex tableName="OFFLINE_CLIENT_SESSION" indexName="IDX_OFFLINE_CSS_PRELOAD" />
<modifySql dbms="mssql">
<!-- ensure that existing rows also get the new values on mssql -->
<!-- https://github.com/liquibase/liquibase/issues/4644 -->
<replace replace="DEFAULT 0" with="DEFAULT 0 WITH VALUES" />
</modifySql>
</changeSet> </changeSet>
<changeSet author="keycloak" id="25.0.0-xxx-2-mysql"> <changeSet author="keycloak" id="25.0.0-28265-index-2-mysql">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN"> <preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<or> <or>
<dbms type="mysql"/> <dbms type="mysql"/>
@ -57,7 +61,7 @@
<column name="REALM_ID" /> <column name="REALM_ID" />
</createIndex> </createIndex>
</changeSet> </changeSet>
<changeSet author="keycloak" id="25.0.0-xxx-2-not-mysql"> <changeSet author="keycloak" id="25.0.0-28265-index-2-not-mysql">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN"> <preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<not> <not>
<or> <or>

View file

@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import org.infinispan.client.hotrod.impl.ConfigurationProperties; import org.infinispan.client.hotrod.impl.ConfigurationProperties;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder; import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfiguration; import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
@ -41,6 +42,7 @@ import org.jgroups.protocols.TCP_NIO2;
import org.jgroups.protocols.UDP; import org.jgroups.protocols.UDP;
import org.jgroups.util.TLS; import org.jgroups.util.TLS;
import org.jgroups.util.TLSClientAuth; import org.jgroups.util.TLSClientAuth;
import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -55,7 +57,10 @@ import static org.keycloak.config.CachingOptions.CACHE_REMOTE_HOST_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PORT_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PORT_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_USERNAME_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_USERNAME_PROPERTY;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.DISTRIBUTED_REPLICATED_CACHE_NAMES; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.DISTRIBUTED_REPLICATED_CACHE_NAMES;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.SCRAM_SHA_512; import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.SCRAM_SHA_512;
@ -103,11 +108,22 @@ public class CacheManagerFactory {
private DefaultCacheManager startCacheManager() { private DefaultCacheManager startCacheManager() {
ConfigurationBuilderHolder builder = new ParserRegistry().parse(config); ConfigurationBuilderHolder builder = new ParserRegistry().parse(config);
if (builder.getNamedConfigurationBuilders().get(USER_SESSION_CACHE_NAME).clustering().cacheMode().isClustered()) { if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) {
configureTransportStack(builder); configureTransportStack(builder);
configureRemoteStores(builder); configureRemoteStores(builder);
} }
DISTRIBUTED_REPLICATED_CACHE_NAMES.forEach(cacheName -> {
if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) &&
(cacheName.equals(USER_SESSION_CACHE_NAME) || cacheName.equals(CLIENT_SESSION_CACHE_NAME) || cacheName.equals(OFFLINE_USER_SESSION_CACHE_NAME) || cacheName.equals(OFFLINE_CLIENT_SESSION_CACHE_NAME))) {
ConfigurationBuilder configurationBuilder = builder.getNamedConfigurationBuilders().get(cacheName);
if (configurationBuilder.memory().maxSize() == null && configurationBuilder.memory().maxCount() == -1) {
logger.infof("Persistent user sessions enabled and no memory limit found in configuration. Setting max entries for %s to 10000 entries", cacheName);
configurationBuilder.memory().maxCount(10000);
}
}
});
if (metricsEnabled) { if (metricsEnabled) {
builder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class); builder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class);
builder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); builder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry);
@ -223,6 +239,11 @@ public class CacheManagerFactory {
} }
DISTRIBUTED_REPLICATED_CACHE_NAMES.forEach(cacheName -> { DISTRIBUTED_REPLICATED_CACHE_NAMES.forEach(cacheName -> {
if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) &&
(cacheName.equals(USER_SESSION_CACHE_NAME) || cacheName.equals(CLIENT_SESSION_CACHE_NAME) || cacheName.equals(OFFLINE_USER_SESSION_CACHE_NAME) || cacheName.equals(OFFLINE_CLIENT_SESSION_CACHE_NAME))) {
return;
}
PersistenceConfigurationBuilder persistenceCB = builder.getNamedConfigurationBuilders().get(cacheName).persistence(); PersistenceConfigurationBuilder persistenceCB = builder.getNamedConfigurationBuilders().get(cacheName).persistence();
//if specified via command line -> cannot be defined in the xml file //if specified via command line -> cannot be defined in the xml file

View file

@ -198,7 +198,11 @@ public interface UserSessionProvider extends Provider {
*/ */
Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults); Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults);
/** Triggered by persister during pre-load. It imports authenticatedClientSessions too **/ /** Triggered by persister during pre-load. It imports authenticatedClientSessions too.
*
* @deprecated Deprecated as offline session preloading was removed in KC25. This method will be removed in KC27.
*/
@Deprecated(forRemoval = true)
void importUserSessions(Collection<UserSessionModel> persistentUserSessions, boolean offline); void importUserSessions(Collection<UserSessionModel> persistentUserSessions, boolean offline);
void close(); void close();

View file

@ -20,3 +20,7 @@ RefreshTokenTest
OfflineTokenTest OfflineTokenTest
AccessTokenTest AccessTokenTest
LogoutTest LogoutTest
ClientStorageTest
UserInfoTest
LightWeightAccessTokenTest
TokenIntrospectionTest

View file

@ -335,6 +335,7 @@ public class OfflineSessionPersistenceTest extends KeycloakModelTest {
private String createOfflineClientSession(String offlineUserSessionId, String clientId) { private String createOfflineClientSession(String offlineUserSessionId, String clientId) {
return withRealm(realmId, (session, realm) -> { return withRealm(realmId, (session, realm) -> {
UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, offlineUserSessionId); UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, offlineUserSessionId);
assertThat("Can't retrieve offline session for " + offlineUserSessionId, offlineUserSession, Matchers.notNullValue());
ClientModel client = session.clients().getClientById(realm, clientId); ClientModel client = session.clients().getClientById(realm, clientId);
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, offlineUserSession); AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, offlineUserSession);
return session.sessions().createOfflineClientSession(clientSession, offlineUserSession).getId(); return session.sessions().createOfflineClientSession(clientSession, offlineUserSession).getId();

View file

@ -481,6 +481,8 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
public void testOfflineSessionLifespanOverride() { public void testOfflineSessionLifespanOverride() {
// skip the test for CrossDC // skip the test for CrossDC
Assume.assumeFalse(Objects.equals(CONFIG.scope("connectionsInfinispan.default").get("remoteStoreEnabled"), "true")); Assume.assumeFalse(Objects.equals(CONFIG.scope("connectionsInfinispan.default").get("remoteStoreEnabled"), "true"));
// As we don't put things in the embedded cache, the test will fail as the number of entries will always be 0.
Assume.assumeFalse(Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE));
createOfflineSessions("user1", 2, new LinkedList<>(), new LinkedList<>()); createOfflineSessions("user1", 2, new LinkedList<>(), new LinkedList<>());