mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Client session may be lost during session restart
Fixes #43349 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
parent
7f17393b52
commit
bb91dbf7ee
@ -18,6 +18,7 @@
|
||||
package org.keycloak.models.sessions.infinispan;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
@ -28,8 +29,6 @@ import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.changes.ClientSessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionsChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.Tasks;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey;
|
||||
@ -42,31 +41,28 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
||||
private final KeycloakSession kcSession;
|
||||
private final AuthenticatedClientSessionEntity entity;
|
||||
private final ClientModel client;
|
||||
private final SessionsChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
|
||||
private final ClientSessionManager clientSessionManager;
|
||||
private UserSessionModel userSession;
|
||||
private final boolean offline;
|
||||
private final EmbeddedClientSessionKey cacheKey;
|
||||
|
||||
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession,
|
||||
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
|
||||
SessionsChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
|
||||
ClientSessionManager clientSessionManager,
|
||||
EmbeddedClientSessionKey cacheKey,
|
||||
boolean offline) {
|
||||
if (userSession == null) {
|
||||
throw new NullPointerException("userSession must not be null");
|
||||
}
|
||||
|
||||
this.userSession = Objects.requireNonNull(userSession, "userSession must not be null");
|
||||
this.kcSession = kcSession;
|
||||
this.entity = entity;
|
||||
this.userSession = userSession;
|
||||
this.client = client;
|
||||
this.clientSessionUpdateTx = clientSessionUpdateTx;
|
||||
this.clientSessionManager = clientSessionManager;
|
||||
this.offline = offline;
|
||||
this.cacheKey = cacheKey;
|
||||
}
|
||||
|
||||
private void update(ClientSessionUpdateTask task) {
|
||||
clientSessionUpdateTx.addTask(cacheKey, task);
|
||||
clientSessionManager.addChange(cacheKey, task);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,9 +80,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
||||
// as nonexistent in org.keycloak.models.sessions.infinispan.UserSessionAdapter.getAuthenticatedClientSessions()
|
||||
this.userSession = null;
|
||||
|
||||
SessionUpdateTask<AuthenticatedClientSessionEntity> removeTask = Tasks.removeSync(offline);
|
||||
|
||||
clientSessionUpdateTx.addTask(cacheKey, removeTask);
|
||||
clientSessionManager.addChange(cacheKey, Tasks.removeSync(offline));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -288,7 +282,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
||||
}
|
||||
};
|
||||
|
||||
update(task);
|
||||
clientSessionManager.restartEntity(cacheKey, task);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.changes.PersistentSessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey;
|
||||
|
||||
/**
|
||||
* Manages transactional context for {@link AuthenticatedClientSessionModel} changes.
|
||||
* <p>
|
||||
* It collects all modifications (the changelog) within the current transaction and applies them to the database only
|
||||
* upon a successful commit.
|
||||
*/
|
||||
public interface ClientSessionManager {
|
||||
|
||||
/**
|
||||
* Adds a update task to the changelog for a specific client session.
|
||||
* <p>
|
||||
* When the transaction commits, this task will apply its changes to the persisted
|
||||
* {@link AuthenticatedClientSessionEntity}, effectively updating the corresponding
|
||||
* {@link AuthenticatedClientSessionModel}. Multiple {@code addChange} calls for the same session are accumulated
|
||||
* (merged).
|
||||
*
|
||||
* @param key The identifier for the target client session.
|
||||
* @param task The operation containing the changes to apply to the persisted entity.
|
||||
* @throws NullPointerException if {@code key} or {@code task} is {@code null}.
|
||||
*/
|
||||
void addChange(EmbeddedClientSessionKey key, PersistentSessionUpdateTask<AuthenticatedClientSessionEntity> task);
|
||||
|
||||
/**
|
||||
* Resets and replaces the state of the persisted {@link AuthenticatedClientSessionEntity} for the given session.
|
||||
* <p>
|
||||
* All previously added changes via {@code addChange} for this session are discarded, and the provided task is
|
||||
* executed to set the new state of the client session entity.
|
||||
*
|
||||
* @param key The identifier for the target client session.
|
||||
* @param task The operation that must set the complete new state of the persisted entity.
|
||||
* @throws NullPointerException if {@code key} or {@code task} is {@code null}.
|
||||
*/
|
||||
void restartEntity(EmbeddedClientSessionKey key, PersistentSessionUpdateTask<AuthenticatedClientSessionEntity> task);
|
||||
|
||||
}
|
||||
@ -57,6 +57,7 @@ import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.light.LightweightUserAdapter;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.PersistentSessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.Tasks;
|
||||
@ -87,7 +88,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class InfinispanUserSessionProvider implements UserSessionProvider, SessionRefreshStore {
|
||||
public class InfinispanUserSessionProvider implements UserSessionProvider, SessionRefreshStore, ClientSessionManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class);
|
||||
|
||||
@ -162,21 +163,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
|
||||
@Override
|
||||
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionUpdateTx = clientSessionTx;
|
||||
final EmbeddedClientSessionKey key = new EmbeddedClientSessionKey(userSession.getId(), client.getId());
|
||||
var entity = AuthenticatedClientSessionEntity.create(realm, client, userSession);
|
||||
|
||||
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, key, false);
|
||||
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, this, key, false);
|
||||
|
||||
// For now, the clientSession is considered transient in case that userSession was transient
|
||||
UserSessionModel.SessionPersistenceState persistenceState = userSession.getPersistenceState() != null ?
|
||||
userSession.getPersistenceState() : UserSessionModel.SessionPersistenceState.PERSISTENT;
|
||||
|
||||
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
|
||||
clientSessionUpdateTx.addTask(key, createClientSessionTask, entity, persistenceState);
|
||||
|
||||
sessionTx.addTask(userSession.getId(), new RegisterClientSessionTask(key.clientId()));
|
||||
|
||||
clientSessionTx.addTask(key, createClientSessionTask, entity, persistenceState);
|
||||
addClientSessionToUserSession(key, false);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@ -694,8 +692,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, EmbeddedClientSessionKey key, boolean offline) {
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
|
||||
return entity != null ? new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, key, offline) : null;
|
||||
return entity != null ? new AuthenticatedClientSessionAdapter(session, entity, client, userSession, this, key, offline) : null;
|
||||
}
|
||||
|
||||
UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) {
|
||||
@ -904,7 +901,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
|
||||
userSessionUpdateTx.addTask(sessionToImportInto.getId(), new RegisterClientSessionTask(clientUUID));
|
||||
|
||||
return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, key, true);
|
||||
return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, this, key, true);
|
||||
}
|
||||
|
||||
|
||||
@ -925,6 +922,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChange(EmbeddedClientSessionKey key, PersistentSessionUpdateTask<AuthenticatedClientSessionEntity> task) {
|
||||
getClientSessionTransaction(task.isOffline()).addTask(key, task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restartEntity(EmbeddedClientSessionKey key, PersistentSessionUpdateTask<AuthenticatedClientSessionEntity> task) {
|
||||
getClientSessionTransaction(task.isOffline()).restartEntity(key, task);
|
||||
addClientSessionToUserSession(key, task.isOffline());
|
||||
}
|
||||
|
||||
private void addClientSessionToUserSession(EmbeddedClientSessionKey cacheKey, boolean offline) {
|
||||
getTransaction(offline).addTask(cacheKey.userSessionId(), new RegisterClientSessionTask(cacheKey.clientId()));
|
||||
}
|
||||
|
||||
private record RegisterClientSessionTask(String clientUuid)
|
||||
implements SessionUpdateTask<UserSessionEntity> {
|
||||
|
||||
|
||||
@ -64,6 +64,7 @@ import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.changes.ClientSessionPersistentChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.JpaChangesPerformer;
|
||||
import org.keycloak.models.sessions.infinispan.changes.MergedUpdate;
|
||||
import org.keycloak.models.sessions.infinispan.changes.PersistentSessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdatesList;
|
||||
@ -93,7 +94,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class PersistentUserSessionProvider implements UserSessionProvider, SessionRefreshStore {
|
||||
public class PersistentUserSessionProvider implements UserSessionProvider, SessionRefreshStore, ClientSessionManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(PersistentUserSessionProvider.class);
|
||||
|
||||
@ -155,9 +156,11 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true");
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionTx, cacheKey, false);
|
||||
final boolean offline = userSession.isOffline();
|
||||
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client,
|
||||
userSession, this, cacheKey, false);
|
||||
|
||||
if (userSession.isOffline()) {
|
||||
if (offline) {
|
||||
// If this is an offline session, and the referred online session doesn't exist anymore, don't register the client session in the transaction.
|
||||
// Instead keep it transient and it will be added to the offline session only afterward. This is expected by SessionTimeoutsTest.testOfflineUserClientIdleTimeoutSmallerThanSessionOneRefresh.
|
||||
if (sessionTx.get(realm, userSession.getId(), userSession, false) == null) {
|
||||
@ -171,7 +174,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
|
||||
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
|
||||
clientSessionTx.addTask(cacheKey, createClientSessionTask, entity, persistenceState);
|
||||
sessionTx.registerClientSession(userSession.getId(), client.getId(), userSession.isOffline());
|
||||
addClientSessionToUserSession(cacheKey, offline);
|
||||
|
||||
return adapter;
|
||||
}
|
||||
@ -301,7 +304,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
var key = new EmbeddedClientSessionKey(userSession.getId(), client.getId());
|
||||
SessionEntityWrapper<AuthenticatedClientSessionEntity> clientSessionEntity = clientSessionTx.get(client.getRealm(), client, userSession, key, offline);
|
||||
if (clientSessionEntity != null) {
|
||||
return new AuthenticatedClientSessionAdapter(session, clientSessionEntity.getEntity(), client, userSession, clientSessionTx, key, offline);
|
||||
return new AuthenticatedClientSessionAdapter(session, clientSessionEntity.getEntity(), client, userSession, this, key, offline);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -779,7 +782,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
sessionToImportInto.getEntity().getClientSessions().add(clientUUID);
|
||||
sessionTx.registerClientSession(sessionToImportInto.getId(), clientUUID, true);
|
||||
|
||||
return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionTx, key, true);
|
||||
return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, this, key, true);
|
||||
}
|
||||
|
||||
public SessionEntityWrapper<UserSessionEntity> wrapPersistentEntity(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) {
|
||||
@ -940,4 +943,18 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
clientSessionPerformer.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChange(EmbeddedClientSessionKey key, PersistentSessionUpdateTask<AuthenticatedClientSessionEntity> task) {
|
||||
clientSessionTx.addTask(key, task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restartEntity(EmbeddedClientSessionKey key, PersistentSessionUpdateTask<AuthenticatedClientSessionEntity> task) {
|
||||
clientSessionTx.restartEntity(key, task);
|
||||
addClientSessionToUserSession(key, task.isOffline());
|
||||
}
|
||||
|
||||
private void addClientSessionToUserSession(EmbeddedClientSessionKey cacheKey, boolean offline) {
|
||||
sessionTx.registerClientSession(cacheKey.userSessionId(), cacheKey.clientId(), offline);
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@ public class UserSessionAdapter<T extends SessionRefreshStore & UserSessionProvi
|
||||
|
||||
};
|
||||
|
||||
update(task);
|
||||
userSessionUpdateTx.restartEntity(getId(), task);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -57,23 +57,22 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
||||
@Override
|
||||
public void addTask(K key, SessionUpdateTask<V> task) {
|
||||
SessionUpdatesList<V> myUpdates = updates.get(key);
|
||||
if (myUpdates == null) {
|
||||
// Lookup entity from cache
|
||||
SessionEntityWrapper<V> wrappedEntity = cacheHolder.cache().get(key);
|
||||
if (wrappedEntity == null) {
|
||||
logger.tracef("Not present cache item for key %s", key);
|
||||
return;
|
||||
}
|
||||
|
||||
RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId());
|
||||
|
||||
myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
|
||||
updates.put(key, myUpdates);
|
||||
if (myUpdates != null) {
|
||||
myUpdates.addAndExecute(task);
|
||||
return;
|
||||
}
|
||||
lookupAndAndExecuteTask(key, task);
|
||||
}
|
||||
|
||||
// Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..)
|
||||
task.runUpdate(myUpdates.getEntityWrapper().getEntity());
|
||||
myUpdates.add(task);
|
||||
@Override
|
||||
public void restartEntity(K key, SessionUpdateTask<V> restartTask) {
|
||||
SessionUpdatesList<V> myUpdates = updates.get(key);
|
||||
if (myUpdates != null) {
|
||||
myUpdates.getUpdateTasks().clear();
|
||||
myUpdates.addAndExecute(restartTask);
|
||||
return;
|
||||
}
|
||||
lookupAndAndExecuteTask(key, restartTask);
|
||||
}
|
||||
|
||||
|
||||
@ -90,8 +89,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
||||
|
||||
if (task != null) {
|
||||
// Run the update now, so reader in same transaction can see it
|
||||
task.runUpdate(entity);
|
||||
myUpdates.add(task);
|
||||
myUpdates.addAndExecute(task);
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,4 +266,20 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
||||
allSessions.forEach((key, wrapper) -> updates.put(key, new SessionUpdatesList<>(realmModel, wrapper)));
|
||||
}
|
||||
|
||||
private void lookupAndAndExecuteTask(K key, SessionUpdateTask<V> task) {
|
||||
// Lookup entity from cache
|
||||
SessionEntityWrapper<V> wrappedEntity = cacheHolder.cache().get(key);
|
||||
if (wrappedEntity == null) {
|
||||
logger.tracef("Not present cache item for key %s", key);
|
||||
return;
|
||||
}
|
||||
|
||||
RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId());
|
||||
|
||||
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
|
||||
updates.put(key, myUpdates);
|
||||
|
||||
// Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..)
|
||||
myUpdates.addAndExecute(task);
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,25 +161,44 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
|
||||
}
|
||||
|
||||
SessionUpdatesList<V> myUpdates = getUpdates(task.isOffline()).get(key);
|
||||
if (myUpdates == null) {
|
||||
// Lookup entity from cache
|
||||
SessionEntityWrapper<V> wrappedEntity = getCache(task.isOffline()).get(key);
|
||||
if (wrappedEntity == null) {
|
||||
LOG.tracef("Not present cache item for key %s", key);
|
||||
return;
|
||||
}
|
||||
// Cache does not contain the offline flag value so adding it
|
||||
wrappedEntity.getEntity().setOffline(task.isOffline());
|
||||
|
||||
RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId());
|
||||
|
||||
myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
|
||||
getUpdates(task.isOffline()).put(key, myUpdates);
|
||||
if (myUpdates != null) {
|
||||
myUpdates.addAndExecute(task);
|
||||
return;
|
||||
}
|
||||
lookupAndAndExecuteTask(key, task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restartEntity(K key, SessionUpdateTask<V> restartTask) {
|
||||
if (!(restartTask instanceof PersistentSessionUpdateTask<V> task)) {
|
||||
throw new IllegalArgumentException("Task must be instance of PersistentSessionUpdateTask");
|
||||
}
|
||||
var myUpdates = getUpdates(task.isOffline()).get(key);
|
||||
if (myUpdates != null) {
|
||||
myUpdates.getUpdateTasks().clear();
|
||||
myUpdates.addAndExecute(task);
|
||||
return;
|
||||
}
|
||||
lookupAndAndExecuteTask(key, task);
|
||||
}
|
||||
|
||||
private void lookupAndAndExecuteTask(K key, PersistentSessionUpdateTask<V> task) {
|
||||
// Lookup entity from cache
|
||||
SessionEntityWrapper<V> wrappedEntity = getCache(task.isOffline()).get(key);
|
||||
if (wrappedEntity == null) {
|
||||
LOG.tracef("Not present cache item for key %s", key);
|
||||
return;
|
||||
}
|
||||
// Cache does not contain the offline flag value so adding it
|
||||
wrappedEntity.getEntity().setOffline(task.isOffline());
|
||||
|
||||
RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId());
|
||||
|
||||
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
|
||||
getUpdates(task.isOffline()).put(key, myUpdates);
|
||||
|
||||
// Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..)
|
||||
task.runUpdate(myUpdates.getEntityWrapper().getEntity());
|
||||
myUpdates.add(task);
|
||||
myUpdates.addAndExecute(task);
|
||||
}
|
||||
|
||||
public void addTask(K key, SessionUpdateTask<V> task, V entity, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
@ -194,8 +213,7 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
|
||||
|
||||
if (task != null) {
|
||||
// Run the update now, so reader in same transaction can see it
|
||||
task.runUpdate(entity);
|
||||
myUpdates.add(task);
|
||||
myUpdates.addAndExecute(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -81,4 +81,9 @@ public class SessionUpdatesList<S extends SessionEntity> {
|
||||
public UserSessionModel.SessionPersistenceState getPersistenceState() {
|
||||
return persistenceState;
|
||||
}
|
||||
|
||||
public void addAndExecute(SessionUpdateTask<S> task) {
|
||||
add(task);
|
||||
task.runUpdate(getEntityWrapper().getEntity());
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,4 +23,6 @@ public interface SessionsChangelogBasedTransaction<K, V extends SessionEntity> {
|
||||
|
||||
void addTask(K key, SessionUpdateTask<V> task);
|
||||
|
||||
void restartEntity(K key, SessionUpdateTask<V> restartTask);
|
||||
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
*/
|
||||
public class Tasks {
|
||||
|
||||
private static final SessionUpdateTask<? extends SessionEntity> ADD_IF_ABSENT_SYNC = new SessionUpdateTask<SessionEntity>() {
|
||||
private static final SessionUpdateTask<? extends SessionEntity> ADD_IF_ABSENT_SYNC = new SessionUpdateTask<>() {
|
||||
@Override
|
||||
public void runUpdate(SessionEntity entity) {
|
||||
}
|
||||
@ -37,7 +37,7 @@ public class Tasks {
|
||||
|
||||
};
|
||||
|
||||
private static final SessionUpdateTask<? extends SessionEntity> REMOVE_SYNC = new PersistentSessionUpdateTask<SessionEntity>() {
|
||||
private static final SessionUpdateTask<? extends SessionEntity> REMOVE_SYNC = new PersistentSessionUpdateTask<>() {
|
||||
@Override
|
||||
public void runUpdate(SessionEntity entity) {
|
||||
}
|
||||
@ -94,8 +94,8 @@ public class Tasks {
|
||||
* @param <S>
|
||||
* @return
|
||||
*/
|
||||
public static <S extends SessionEntity> SessionUpdateTask<S> removeSync(boolean offline) {
|
||||
return offline ? (SessionUpdateTask<S>) OFFLINE_REMOVE_SYNC : (SessionUpdateTask<S>) REMOVE_SYNC;
|
||||
public static <S extends SessionEntity> PersistentSessionUpdateTask<S> removeSync(boolean offline) {
|
||||
return offline ? (PersistentSessionUpdateTask<S>) OFFLINE_REMOVE_SYNC : (PersistentSessionUpdateTask<S>) REMOVE_SYNC;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ public abstract class BaseUpdater<K, V> implements Updater<K, V> {
|
||||
private final K cacheKey;
|
||||
private final V cacheValue;
|
||||
private final long versionRead;
|
||||
private final UpdaterState initialState;
|
||||
private UpdaterState state;
|
||||
|
||||
protected BaseUpdater(K cacheKey, V cacheValue, long versionRead, UpdaterState state) {
|
||||
@ -41,6 +42,7 @@ public abstract class BaseUpdater<K, V> implements Updater<K, V> {
|
||||
this.cacheValue = cacheValue;
|
||||
this.versionRead = versionRead;
|
||||
this.state = Objects.requireNonNull(state);
|
||||
this.initialState = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -110,6 +112,13 @@ public abstract class BaseUpdater<K, V> implements Updater<K, V> {
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the {@link UpdaterState} to its initial value.
|
||||
*/
|
||||
protected final void resetState() {
|
||||
state = initialState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the entity was changed after being created/read.
|
||||
*/
|
||||
|
||||
@ -210,6 +210,8 @@ public class AuthenticatedClientSessionUpdater extends BaseUpdater<ClientSession
|
||||
|
||||
@Override
|
||||
public void restartClientSession() {
|
||||
changes.clear();
|
||||
resetState();
|
||||
addAndApplyChange(RemoteAuthenticatedClientSessionEntity::restart);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.changes.remote.updater.user;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
|
||||
/**
|
||||
* It gives a read-only view of the {@link AuthenticatedClientSessionModel} belonging to a
|
||||
* {@link org.keycloak.models.UserSessionModel} though the {@link Map} interface where the key is the Client ID.
|
||||
* <p>
|
||||
* Any direct modification via the {@link Map} interface will throw an {@link UnsupportedOperationException}. To add a
|
||||
* new mapping, use a method like {@link UserSessionProvider#createClientSession(RealmModel, ClientModel, UserSessionModel)} or
|
||||
* equivalent. To remove a mapping, use {@link AuthenticatedClientSessionModel#detachFromUserSession()}.
|
||||
*/
|
||||
public interface AuthenticatedClientSessionMapping extends Map<String, AuthenticatedClientSessionModel> {
|
||||
|
||||
/**
|
||||
* Notifies the associated {@link UserSessionModel} has been restarted.
|
||||
* <p>
|
||||
* All the {@link AuthenticatedClientSessionModel} must be detached.
|
||||
*/
|
||||
void onUserSessionRestart();
|
||||
|
||||
}
|
||||
@ -33,7 +33,7 @@ public class UserSessionUpdater extends BaseUpdater<String, RemoteUserSessionEnt
|
||||
private final boolean offline;
|
||||
private RealmModel realm;
|
||||
private UserModel user;
|
||||
private Map<String, AuthenticatedClientSessionModel> clientSessions;
|
||||
private AuthenticatedClientSessionMapping clientSessions;
|
||||
private SessionPersistenceState persistenceState = SessionPersistenceState.PERSISTENT;
|
||||
|
||||
private UserSessionUpdater(String cacheKey, RemoteUserSessionEntity cacheValue, long version, boolean offline, UpdaterState initialState) {
|
||||
@ -205,7 +205,8 @@ public class UserSessionUpdater extends BaseUpdater<String, RemoteUserSessionEnt
|
||||
this.user = user;
|
||||
changes.clear();
|
||||
notesUpdater.clear();
|
||||
clientSessions.clear();
|
||||
clientSessions.onUserSessionRestart();
|
||||
resetState();
|
||||
addAndApplyChange(userSessionEntity -> userSessionEntity.restart(realm.getId(), user.getId(), loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId));
|
||||
}
|
||||
|
||||
@ -232,7 +233,7 @@ public class UserSessionUpdater extends BaseUpdater<String, RemoteUserSessionEnt
|
||||
* @param user The {@link UserModel} associated to this user session.
|
||||
* @param clientSessions The {@link Map} associated to this use session.
|
||||
*/
|
||||
public synchronized void initialize(SessionPersistenceState persistenceState, RealmModel realm, UserModel user, Map<String, AuthenticatedClientSessionModel> clientSessions) {
|
||||
public synchronized void initialize(SessionPersistenceState persistenceState, RealmModel realm, UserModel user, AuthenticatedClientSessionMapping clientSessions) {
|
||||
this.realm = Objects.requireNonNull(realm);
|
||||
this.user = Objects.requireNonNull(user);
|
||||
this.persistenceState = Objects.requireNonNull(persistenceState);
|
||||
|
||||
@ -37,6 +37,7 @@ public final class ClientSessionQueries {
|
||||
private static final String PER_CLIENT_COUNT = "SELECT e.clientId, count(e.clientId) FROM %s as e GROUP BY e.clientId ORDER BY e.clientId".formatted(CLIENT_SESSION);
|
||||
private static final String CLIENT_SESSION_COUNT = "SELECT count(e) FROM %s as e WHERE e.realmId = :realmId && e.clientId = :clientId".formatted(CLIENT_SESSION);
|
||||
private static final String FROM_USER_SESSION = "FROM %s as e WHERE e.userSessionId = :userSessionId ORDER BY e.clientId".formatted(CLIENT_SESSION);
|
||||
private static final String IDS_FROM_USER_SESSION = "SELECT e.clientId FROM %s as e WHERE e.userSessionId = :userSessionId ORDER BY e.clientId".formatted(CLIENT_SESSION);
|
||||
|
||||
/**
|
||||
* Returns a projection with the user session ID for client sessions from the client {@code clientId}.
|
||||
@ -72,5 +73,15 @@ public final class ClientSessionQueries {
|
||||
.setParameter("userSessionId", userSessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a projection with the client IDs belonging to the user session.
|
||||
* <p>
|
||||
* The returned array contains a single {@link String} element with the client ID.
|
||||
*/
|
||||
public static Query<Object[]> fetchClientSessionsIds(RemoteCache<ClientSessionKey, RemoteAuthenticatedClientSessionEntity> cache, String userSessionId) {
|
||||
return cache.<Object[]>query(IDS_FROM_USER_SESSION)
|
||||
.setParameter("userSessionId", userSessionId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ import org.keycloak.models.light.LightweightUserAdapter;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.changes.remote.updater.BaseUpdater;
|
||||
import org.keycloak.models.sessions.infinispan.changes.remote.updater.client.AuthenticatedClientSessionUpdater;
|
||||
import org.keycloak.models.sessions.infinispan.changes.remote.updater.user.AuthenticatedClientSessionMapping;
|
||||
import org.keycloak.models.sessions.infinispan.changes.remote.updater.user.UserSessionUpdater;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientSessionKey;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RemoteAuthenticatedClientSessionEntity;
|
||||
@ -468,7 +469,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
return updater;
|
||||
}
|
||||
|
||||
private class ClientSessionMapping extends AbstractMap<String, AuthenticatedClientSessionModel> implements Consumer<RemoteAuthenticatedClientSessionEntity> {
|
||||
private class ClientSessionMapping extends AbstractMap<String, AuthenticatedClientSessionModel> implements Consumer<RemoteAuthenticatedClientSessionEntity>, AuthenticatedClientSessionMapping {
|
||||
|
||||
private final UserSessionUpdater userSession;
|
||||
private boolean coldCache = true;
|
||||
@ -477,11 +478,6 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
this.userSession = userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
getTransaction().removeByUserSessionId(getUserSessionId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticatedClientSessionModel get(Object key) {
|
||||
var updater = getTransaction().get(keyForClientId(key));
|
||||
@ -523,6 +519,11 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
return keyForClientId(String.valueOf(clientId));
|
||||
}
|
||||
|
||||
private ClientSessionKey keyForClientId(Object[] projection) {
|
||||
assert projection.length == 1;
|
||||
return keyForClientId(String.valueOf(projection[0]));
|
||||
}
|
||||
|
||||
private void fetchAndCacheClientSessions() {
|
||||
var query = ClientSessionQueries.fetchClientSessions(getTransaction().getCache(), getUserSessionId());
|
||||
QueryHelper.streamAll(query, batchSize, Function.identity()).forEach(this);
|
||||
@ -548,6 +549,21 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
private AuthenticatedClientSessionModel initialize(AuthenticatedClientSessionUpdater updater) {
|
||||
return initClientSessionUpdater(updater, userSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSessionRestart() {
|
||||
if (coldCache) {
|
||||
// not all sessions cached in the transaction, we fetch the client ID and mark all them as deleted.
|
||||
var query = ClientSessionQueries.fetchClientSessionsIds(getTransaction().getCache(), getUserSessionId());
|
||||
QueryHelper.streamAll(query, batchSize, this::keyForClientId)
|
||||
.forEach(getTransaction()::remove);
|
||||
coldCache = false;
|
||||
return;
|
||||
}
|
||||
getTransaction().getClientSessions()
|
||||
.filter(this::isFromUserSession)
|
||||
.forEach(BaseUpdater::markDeleted);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map.Entry<String, AuthenticatedClientSessionModel> toMapEntry(AuthenticatedClientSessionModel model) {
|
||||
|
||||
@ -76,7 +76,10 @@ public interface UserSessionModel {
|
||||
|
||||
/**
|
||||
* Returns map where key is ID of the client (its UUID) and value is ID respective {@link AuthenticatedClientSessionModel} object.
|
||||
* @return
|
||||
* <p>
|
||||
* Any direct modification via the {@link Map} interface will throw an {@link UnsupportedOperationException}. To add a
|
||||
* new mapping, use a method like {@link UserSessionProvider#createClientSession(RealmModel, ClientModel, UserSessionModel)} or
|
||||
* equivalent. To remove a mapping, use {@link AuthenticatedClientSessionModel#detachFromUserSession()}.
|
||||
*/
|
||||
Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions();
|
||||
/**
|
||||
|
||||
@ -147,23 +147,30 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||
try {
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), kcSession -> {
|
||||
kcSession.getContext().setRealm(realm);
|
||||
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessions[0].getId());
|
||||
assertSession(userSession, kcSession.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
|
||||
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessions[1].getId());
|
||||
assertSession(userSession, kcSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId());
|
||||
assertNotNull(clientSession);
|
||||
|
||||
// add some dummy session notes just to test if restart bellow will work
|
||||
userSession.setNote("k1", "v1");
|
||||
userSession.setNote("k2", "v2");
|
||||
userSession.setNote("k3", "v3");
|
||||
|
||||
// A user session can be restarted AuthenticationProcessor.attachSession()
|
||||
// It invokes TokenManager.attachAuthenticationSession() that will restart an existing client session (if not valid) or create a new one.
|
||||
// We test both cases here. "test-app" is restarted and "third-party" is a new client session
|
||||
userSession.restartSession(realm, kcSession.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.6", "form", true, null, null);
|
||||
clientSession.restartClientSession();
|
||||
createClientSession(kcSession, realm.getClientByClientId("third-party"), sessions[1], "http://redirect", "state");
|
||||
});
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), kcSession -> {
|
||||
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessions[0].getId());
|
||||
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessions[1].getId());
|
||||
|
||||
assertThat(userSession.getNotes(), Matchers.anEmptyMap());
|
||||
|
||||
assertSession(userSession, kcSession.users().getUserByUsername(realm, "user2"), "127.0.0.6", started + 100, started + 100);
|
||||
assertSession(userSession, kcSession.users().getUserByUsername(realm, "user2"), "127.0.0.6", started + 100, started + 100, "test-app", "third-party");
|
||||
});
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user