Persist online sessions to the database (#27977)

Adding two feature toggles for new code paths to store online sessions in the existing offline sessions table. Separate the code which is due to be changed in the next iteration in new classes/providers which used instead of the old one.

Closes #27976

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Signed-off-by: Michal Hajas <mhajas@redhat.com>
Co-authored-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-03-28 09:17:07 +01:00 committed by GitHub
parent 757c524cc5
commit c580c88c93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 4633 additions and 178 deletions

View file

@ -303,6 +303,63 @@ jobs:
with:
job-id: jdk-integration-tests-${{ matrix.os }}-${{ matrix.dist }}-${{ matrix.version }}
persistent-sessions-tests:
name: Persistent Sessions IT
needs: [build, conditional]
if: needs.conditional.outputs.ci-store == 'true'
runs-on: ubuntu-latest
timeout-minutes: 150
steps:
- uses: actions/checkout@v4
- id: integration-test-setup
name: Integration test setup
uses: ./.github/actions/integration-test-setup
- name: Run base tests
run: |
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh persistent-sessions`
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
- name: Upload JVM Heapdumps
if: always()
uses: ./.github/actions/upload-heapdumps
- uses: ./.github/actions/upload-flaky-tests
name: Upload flaky tests
env:
GH_TOKEN: ${{ github.token }}
with:
job-name: Store IT
- name: Surefire reports
if: always()
uses: ./.github/actions/archive-surefire-reports
with:
job-id: store-integration-tests-${{ matrix.db }}
- name: EC2 Maven Logs
if: failure()
uses: actions/upload-artifact@v3
with:
name: store-it-mvn-logs
path: .github/scripts/ansible/files
- name: Delete Aurora EC2 Instance
if: ${{ always() && matrix.db == 'aurora-postgres' }}
working-directory: .github/scripts/ansible
run: |
export CLUSTER_NAME=${{ steps.aurora-tests.outputs.ec2_cluster }}
./aws_ec2.sh delete ${{ steps.aurora-init.outputs.region }}
- name: Delete Aurora DB
if: ${{ always() && matrix.db == 'aurora-postgres' }}
uses: ./.github/actions/aurora-delete-database
with:
name: ${{ steps.aurora-init.outputs.name }}
region: ${{ steps.aurora-init.outputs.region }}
store-integration-tests:
name: Store IT
needs: [build, conditional]
@ -757,6 +814,7 @@ jobs:
- quarkus-integration-tests
- jdk-integration-tests
- store-integration-tests
- persistent-sessions-tests
- store-model-tests
- clustering-integration-tests
- fips-unit-tests

View file

@ -109,6 +109,9 @@ public class Profile {
HOSTNAME_V1("Hostname Options V1", Type.DEFAULT),
//HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2),
PERSISTENT_USER_SESSIONS("Persistent online user sessions across restarts and upgrades", Type.EXPERIMENTAL),
PERSISTENT_USER_SESSIONS_NO_CACHE("No caching for online user sessions when they are persisted", Type.EXPERIMENTAL),
OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL),
DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL),

View file

@ -32,10 +32,9 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.ClientSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.Tasks;
import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshChecker;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.util.UUID;
/**
@ -44,14 +43,14 @@ import java.util.UUID;
public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
private final KeycloakSession kcSession;
private final InfinispanUserSessionProvider provider;
private final SessionRefreshStore provider;
private AuthenticatedClientSessionEntity entity;
private final ClientModel client;
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
private UserSessionModel userSession;
private boolean offline;
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, SessionRefreshStore provider,
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
if (userSession == null) {

View file

@ -90,7 +90,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InfinispanUserSessionProvider implements UserSessionProvider {
public class InfinispanUserSessionProvider implements UserSessionProvider, SessionRefreshStore {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class);
@ -176,15 +176,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return offline ? offlineClientSessionTx : clientSessionTx;
}
protected CrossDCLastSessionRefreshStore getLastSessionRefreshStore() {
@Override
public CrossDCLastSessionRefreshStore getLastSessionRefreshStore() {
return lastSessionRefreshStore;
}
protected CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() {
@Override
public CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() {
return offlineLastSessionRefreshStore;
}
protected PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() {
@Override
public PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() {
return persisterLastSessionRefreshStore;
}
@ -244,7 +247,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return adapter;
}
void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
static void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
entity.setRealmId(realm.getId());
entity.setUser(user.getId());
entity.setLoginUsername(loginUsername);

View file

@ -23,6 +23,7 @@ import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Environment;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
@ -94,13 +95,29 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
private InfinispanKeyGenerator keyGenerator;
@Override
public InfinispanUserSessionProvider create(KeycloakSession session) {
public UserSessionProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
return new PersistentUserSessionProvider(
session,
remoteCacheInvoker,
lastSessionRefreshStore,
offlineLastSessionRefreshStore,
persisterLastSessionRefreshStore,
keyGenerator,
cache,
offlineSessionsCache,
clientSessionCache,
offlineClientSessionsCache,
this::deriveOfflineSessionCacheEntryLifespanMs,
this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs
);
}
return new InfinispanUserSessionProvider(
session,
remoteCacheInvoker,
@ -148,8 +165,15 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
} else if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
UserSessionProvider provider1 = userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
if (provider1 instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else if (provider1 instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else {
throw new IllegalStateException("Unknown provider type: " + provider1.getClass());
}
} else if (event instanceof ResetTimeOffsetEvent) {
if (persisterLastSessionRefreshStore != null) {
persisterLastSessionRefreshStore.reset();
@ -212,6 +236,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId());
} else if (provider instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId());
}
}
@ -224,6 +250,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
} else if (provider instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
}
}
@ -236,6 +264,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId());
} else if (provider instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId());
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2024 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.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStore;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore;
public interface SessionRefreshStore {
CrossDCLastSessionRefreshStore getLastSessionRefreshStore();
CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore();
PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore();
}

View file

@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshChecker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
@ -43,16 +44,14 @@ import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserSessionAdapter implements UserSessionModel {
public class UserSessionAdapter<T extends SessionRefreshStore & UserSessionProvider> implements UserSessionModel {
private final KeycloakSession session;
private final InfinispanUserSessionProvider provider;
private final T provider;
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
@ -68,7 +67,7 @@ public class UserSessionAdapter implements UserSessionModel {
private SessionPersistenceState persistenceState;
public UserSessionAdapter(KeycloakSession session, UserModel user, InfinispanUserSessionProvider provider,
public UserSessionAdapter(KeycloakSession session, UserModel user, T provider,
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
RealmModel realm, UserSessionEntity entity, boolean offline) {
@ -94,7 +93,7 @@ public class UserSessionAdapter implements UserSessionModel {
// Check if client still exists
ClientModel client = realm.getClientById(key);
if (client != null) {
final AuthenticatedClientSessionAdapter clientSession = provider.getClientSession(this, client, value.toString(), offline);
final AuthenticatedClientSessionModel clientSession = provider.getClientSession(this, client, value.toString(), offline);
if (clientSession != null) {
result.put(key, clientSession);
}
@ -330,7 +329,7 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public void runUpdate(UserSessionEntity entity) {
provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
InfinispanUserSessionProvider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
entity.setState(null);
entity.getNotes().clear();
@ -360,7 +359,8 @@ public class UserSessionAdapter implements UserSessionModel {
return getId().hashCode();
}
UserSessionEntity getEntity() {
// TODO: This should not be public
public UserSessionEntity getEntity() {
return entity;
}

View file

@ -0,0 +1,191 @@
/*
* Copyright 2024 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;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider;
import org.keycloak.models.sessions.infinispan.SessionFunction;
import org.keycloak.models.sessions.infinispan.UserSessionAdapter;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class ClientSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> {
private static final Logger LOG = Logger.getLogger(ClientSessionPersistentChangelogBasedTransaction.class);
private final InfinispanKeyGenerator keyGenerator;
private final UserSessionPersistentChangelogBasedTransaction userSessionTx;
public ClientSessionPersistentChangelogBasedTransaction(KeycloakSession session, Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction<AuthenticatedClientSessionEntity> lifespanMsLoader, SessionFunction<AuthenticatedClientSessionEntity> maxIdleTimeMsLoader, boolean offline, InfinispanKeyGenerator keyGenerator, UserSessionPersistentChangelogBasedTransaction userSessionTx) {
super(session, cache, remoteCacheInvoker, lifespanMsLoader, maxIdleTimeMsLoader, offline);
this.keyGenerator = keyGenerator;
this.userSessionTx = userSessionTx;
}
public SessionEntityWrapper<AuthenticatedClientSessionEntity> get(RealmModel realm, ClientModel client, UserSessionModel userSession, UUID key) {
SessionUpdatesList<AuthenticatedClientSessionEntity> myUpdates = updates.get(key);
if (myUpdates == null) {
SessionEntityWrapper<AuthenticatedClientSessionEntity> wrappedEntity = cache.get(key);
if (wrappedEntity == null) {
wrappedEntity = getSessionEntityFromPersister(realm, client, userSession);
}
if (wrappedEntity == null) {
return null;
}
RealmModel realmFromSession = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId());
if (!realmFromSession.getId().equals(realm.getId())) {
LOG.warnf("Realm mismatch for session %s. Expected realm %s, but found realm %s", wrappedEntity.getEntity(), realm.getId(), realmFromSession.getId());
return null;
}
myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
updates.put(key, myUpdates);
return wrappedEntity;
} else {
AuthenticatedClientSessionEntity entity = myUpdates.getEntityWrapper().getEntity();
// If entity is scheduled for remove, we don't return it.
boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> {
return task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE;
}).findFirst().isPresent();
return scheduledForRemove ? null : myUpdates.getEntityWrapper();
}
}
private SessionEntityWrapper<AuthenticatedClientSessionEntity> getSessionEntityFromPersister(RealmModel realm, ClientModel client, UserSessionModel userSession) {
UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class);
AuthenticatedClientSessionModel clientSession = persister.loadClientSession(realm, client, userSession, offline);
if (clientSession == null) {
return null;
}
SessionEntityWrapper<AuthenticatedClientSessionEntity> authenticatedClientSessionEntitySessionEntityWrapper = importClientSession(realm, client, userSession, clientSession);
if (authenticatedClientSessionEntitySessionEntityWrapper == null) {
persister.removeClientSession(userSession.getId(), client.getId(), offline);
}
return authenticatedClientSessionEntitySessionEntityWrapper;
}
private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession,
String realmId, String clientId) {
UUID clientSessionId = null;
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);
entity.setRealmId(realmId);
entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol());
entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes());
entity.setClientId(clientId);
entity.setRedirectUri(clientSession.getRedirectUri());
entity.setTimestamp(clientSession.getTimestamp());
return entity;
}
private SessionEntityWrapper<AuthenticatedClientSessionEntity> importClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionModel persistentClientSession) {
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), persistentClientSession,
realm.getId(), client.getId());
entity.setUserSessionId(userSession.getId());
// Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value
entity.setTimestamp(userSession.getLastSessionRefresh());
if (maxIdleTimeMsLoader.apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG
|| lifespanMsLoader.apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) {
return null;
}
final UUID clientSessionId = entity.getId();
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
this.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
if (! (userSession instanceof UserSessionAdapter)) {
throw new IllegalStateException("UserSessionModel must be instance of UserSessionAdapter");
}
UserSessionAdapter sessionToImportInto = (UserSessionAdapter) userSession;
AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions();
clientSessions.put(client.getId(), clientSessionId);
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId);
userSessionTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
return new SessionEntityWrapper<>(entity);
}
private static class RegisterClientSessionTask implements SessionUpdateTask<UserSessionEntity> {
private final String clientUuid;
private final UUID clientSessionId;
public RegisterClientSessionTask(String clientUuid, UUID clientSessionId) {
this.clientUuid = clientUuid;
this.clientSessionId = clientSessionId;
}
@Override
public void runUpdate(UserSessionEntity session) {
AuthenticatedClientSessionStore clientSessions = session.getAuthenticatedClientSessions();
clientSessions.put(clientUuid, clientSessionId);
}
@Override
public CacheOperation getOperation(UserSessionEntity session) {
return CacheOperation.REPLACE;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return CrossDCMessageStatus.SYNC;
}
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright 2024 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;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.InfinispanUtil;
import org.keycloak.models.sessions.infinispan.CacheDecorators;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class EmbeddedCachesChangesPerformer<K, V extends SessionEntity> implements SessionChangesPerformer<K, V> {
private static final Logger LOG = Logger.getLogger(EmbeddedCachesChangesPerformer.class);
private final Cache<K, SessionEntityWrapper<V>> cache;
private final List<Runnable> changes = new LinkedList<>();
public EmbeddedCachesChangesPerformer(Cache<K, SessionEntityWrapper<V>> cache) {
this.cache = cache;
}
private void runOperationInCluster(K key, MergedUpdate<V> task, SessionEntityWrapper<V> sessionWrapper) {
V session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
// Don't need to run update of underlying entity. Local updates were already run
//task.runUpdate(session);
switch (operation) {
case REMOVE:
// Just remove it
CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache)
.withFlags(Flag.IGNORE_RETURN_VALUES)
.remove(key);
break;
case ADD:
CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache)
.withFlags(Flag.IGNORE_RETURN_VALUES)
.put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS);
LOG.tracef("Added entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs());
break;
case ADD_IF_ABSENT:
SessionEntityWrapper<V> existing = CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache).putIfAbsent(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS);
if (existing != null) {
LOG.debugf("Existing entity in cache for key: %s . Will update it", key);
// Apply updates on the existing entity and replace it
task.runUpdate(existing.getEntity());
replace(key, task, existing, task.getLifespanMs(), task.getMaxIdleTimeMs());
} else {
LOG.tracef("Add_if_absent successfully called for entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs());
}
break;
case REPLACE:
replace(key, task, sessionWrapper, task.getLifespanMs(), task.getMaxIdleTimeMs());
break;
default:
throw new IllegalStateException("Unsupported state " + operation);
}
}
private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity, long lifespanMs, long maxIdleTimeMs) {
boolean replaced = false;
int iteration = 0;
V session = oldVersionEntity.getEntity();
while (!replaced && iteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) {
iteration++;
SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
// Atomic cluster-aware replace
replaced = CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache).replace(key, oldVersionEntity, newVersionEntity, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS);
// Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again
if (!replaced) {
if (LOG.isDebugEnabled()) {
LOG.debugf("Replace failed for entity: %s, old version %s, new version %s. Will try again", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion());
}
oldVersionEntity = cache.get(key);
if (oldVersionEntity == null) {
LOG.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key);
return;
}
session = oldVersionEntity.getEntity();
task.runUpdate(session);
} else {
if (LOG.isTraceEnabled()) {
LOG.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s, Lifespan: %d ms, MaxIdle: %d ms", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion(), task.getLifespanMs(), task.getMaxIdleTimeMs());
}
}
}
if (!replaced) {
LOG.warnf("Failed to replace entity '%s' in cache '%s'", key, cache.getName());
}
}
private SessionEntityWrapper<V> generateNewVersionAndWrapEntity(V entity, Map<String, String> localMetadata) {
return new SessionEntityWrapper<>(localMetadata, entity);
}
@Override
public void registerChange(Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged) {
changes.add(() -> runOperationInCluster(entry.getKey(), merged, entry.getValue().getEntityWrapper()));
}
@Override
public void applyChanges() {
changes.forEach(Runnable::run);
}
}

View file

@ -19,11 +19,13 @@ package org.keycloak.models.sessions.infinispan.changes;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -34,6 +36,9 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.connections.infinispan.InfinispanUtil;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -41,15 +46,15 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
private final KeycloakSession kcSession;
protected final KeycloakSession kcSession;
private final String cacheName;
private final Cache<K, SessionEntityWrapper<V>> cache;
protected final Cache<K, SessionEntityWrapper<V>> cache;
private final RemoteCacheInvoker remoteCacheInvoker;
private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
protected final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
private final SessionFunction<V> lifespanMsLoader;
private final SessionFunction<V> maxIdleTimeMsLoader;
protected final SessionFunction<V> lifespanMsLoader;
protected final SessionFunction<V> maxIdleTimeMsLoader;
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker,
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
@ -65,6 +70,10 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
public void addTask(K key, SessionUpdateTask<V> task) {
SessionUpdatesList<V> myUpdates = updates.get(key);
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))) {
throw new IllegalStateException("Can't load from cache");
}
// Lookup entity from cache
SessionEntityWrapper<V> wrappedEntity = cache.get(key);
if (wrappedEntity == null) {
@ -95,10 +104,12 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity, persistenceState);
updates.put(key, myUpdates);
if (task != null) {
// Run the update now, so reader in same transaction can see it
task.runUpdate(entity);
myUpdates.add(task);
}
}
public void reloadEntityInCurrentTransaction(RealmModel realm, K key, SessionEntityWrapper<V> entity) {
@ -150,7 +161,6 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
}
}
@Override
protected void commitImpl() {
for (Map.Entry<K, SessionUpdatesList<V>> entry : updates.entrySet()) {

View file

@ -0,0 +1,707 @@
/*
* Copyright 2024 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;
import org.infinispan.util.function.TriConsumer;
import org.keycloak.common.util.Retry;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.delegate.ClientModelLazyDelegate;
import org.keycloak.models.session.PersistentAuthenticatedClientSessionAdapter;
import org.keycloak.models.session.PersistentUserSessionAdapter;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmModelDelegate;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.models.utils.UserSessionModelDelegate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionChangesPerformer<K, V> {
private final KeycloakSession session;
private final String cacheName;
private final boolean offline;
private final List<Consumer<KeycloakSession>> changes = new LinkedList<>();
private final TriConsumer<KeycloakSession, Map.Entry<K, SessionUpdatesList<V>>, MergedUpdate<V>> processor;
public JpaChangesPerformer(KeycloakSession session, String cacheName, boolean offline) {
this.session = session;
this.cacheName = cacheName;
this.offline = offline;
processor = processor();
}
@Override
public void registerChange(Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged) {
changes.add(innerSession -> processor.accept(innerSession, entry, merged));
}
private TriConsumer<KeycloakSession, Map.Entry<K, SessionUpdatesList<V>>, MergedUpdate<V>> processor() {
return switch (cacheName) {
case "sessions", "offlineSessions" -> this::processUserSessionUpdate;
case "clientSessions", "offlineClientSessions" -> this::processClientSessionUpdate;
default -> throw new IllegalStateException("Unexpected value: " + cacheName);
};
}
@Override
public void applyChanges() {
Retry.executeWithBackoff(iteration -> KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(),
innerSession -> changes.forEach(c -> c.accept(innerSession))),
10, 10);
}
private void processClientSessionUpdate(KeycloakSession innerSession, Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged) {
SessionUpdatesList<V> sessionUpdates = entry.getValue();
SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
RealmModel realm = sessionUpdates.getRealm();
UserSessionPersisterProvider userSessionPersister = innerSession.getProvider(UserSessionPersisterProvider.class);
if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.REMOVE) {
AuthenticatedClientSessionEntity entity = (AuthenticatedClientSessionEntity) sessionWrapper.getEntity();
userSessionPersister.removeClientSession(entity.getUserSessionId(), entity.getClientId(), offline);
} else if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD || merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD_IF_ABSENT){
AuthenticatedClientSessionEntity entity = (AuthenticatedClientSessionEntity) sessionWrapper.getEntity();
userSessionPersister.createClientSession(new AuthenticatedClientSessionModel() {
@Override
public int getStarted() {
return entity.getStarted();
}
@Override
public int getUserSessionStarted() {
return entity.getUserSessionStarted();
}
@Override
public boolean isUserSessionRememberMe() {
return entity.isUserSessionRememberMe();
}
@Override
public String getId() {
return entity.getId().toString();
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public void setTimestamp(int timestamp) {
throw new IllegalStateException("not implemented");
}
@Override
public void detachFromUserSession() {
throw new IllegalStateException("not implemented");
}
@Override
public UserSessionModel getUserSession() {
return new UserSessionModelDelegate(null) {
@Override
public String getId() {
return entity.getUserSessionId();
}
};
}
@Override
public String getCurrentRefreshToken() {
return entity.getCurrentRefreshToken();
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
throw new IllegalStateException("not implemented");
}
@Override
public int getCurrentRefreshTokenUseCount() {
return entity.getCurrentRefreshTokenUseCount();
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
throw new IllegalStateException("not implemented");
}
@Override
public String getNote(String name) {
return entity.getNotes().get(name);
}
@Override
public void setNote(String name, String value) {
throw new IllegalStateException("not implemented");
}
@Override
public void removeNote(String name) {
throw new IllegalStateException("not implemented");
}
@Override
public Map<String, String> getNotes() {
return entity.getNotes();
}
@Override
public String getRedirectUri() {
return entity.getRedirectUri();
}
@Override
public void setRedirectUri(String uri) {
throw new IllegalStateException("not implemented");
}
@Override
public RealmModel getRealm() {
return innerSession.realms().getRealm(entity.getRealmId());
}
@Override
public ClientModel getClient() {
return new ClientModelLazyDelegate(() -> null) {
@Override
public String getId() {
return entity.getClientId();
}
};
}
@Override
public String getAction() {
return entity.getAction();
}
@Override
public void setAction(String action) {
throw new IllegalStateException("not implemented");
}
@Override
public String getProtocol() {
return entity.getAuthMethod();
}
@Override
public void setProtocol(String method) {
throw new IllegalStateException("not implemented");
}
}, offline);
} else {
AuthenticatedClientSessionEntity entity = (AuthenticatedClientSessionEntity) sessionWrapper.getEntity();
ClientModel client = new ClientModelLazyDelegate(null) {
@Override
public String getId() {
return entity.getClientId();
}
};
UserSessionModel userSession = new UserSessionModelDelegate(null) {
@Override
public String getId() {
return entity.getUserSessionId();
}
};
PersistentAuthenticatedClientSessionAdapter clientSessionModel = (PersistentAuthenticatedClientSessionAdapter) userSessionPersister.loadClientSession(realm, client, userSession, offline);
if (clientSessionModel != null) {
AuthenticatedClientSessionEntity authenticatedClientSessionEntity = new AuthenticatedClientSessionEntity(entity.getId()) {
@Override
public Map<String, String> getNotes() {
return new HashMap<>() {
@Override
public String get(Object key) {
return clientSessionModel.getNotes().get(key);
}
@Override
public String put(String key, String value) {
String oldValue = clientSessionModel.getNotes().get(key);
clientSessionModel.setNote(key, value);
return oldValue;
}
};
}
@Override
public void setRedirectUri(String redirectUri) {
clientSessionModel.setRedirectUri(redirectUri);
}
@Override
public void setTimestamp(int timestamp) {
clientSessionModel.setTimestamp(timestamp);
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
clientSessionModel.setCurrentRefreshToken(currentRefreshToken);
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
clientSessionModel.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override
public void setAction(String action) {
clientSessionModel.setAction(action);
}
@Override
public void setAuthMethod(String authMethod) {
clientSessionModel.setProtocol(authMethod);
}
@Override
public String getAuthMethod() {
throw new IllegalStateException("not implemented");
}
@Override
public String getRedirectUri() {
return clientSessionModel.getRedirectUri();
}
@Override
public int getTimestamp() {
return clientSessionModel.getTimestamp();
}
@Override
public int getUserSessionStarted() {
return clientSessionModel.getUserSessionStarted();
}
@Override
public int getStarted() {
return clientSessionModel.getStarted();
}
@Override
public boolean isUserSessionRememberMe() {
return clientSessionModel.isUserSessionRememberMe();
}
@Override
public String getClientId() {
return clientSessionModel.getClient().getClientId();
}
@Override
public void setClientId(String clientId) {
throw new IllegalStateException("not implemented");
}
@Override
public String getAction() {
return clientSessionModel.getAction();
}
@Override
public void setNotes(Map<String, String> notes) {
clientSessionModel.getNotes().keySet().forEach(clientSessionModel::removeNote);
notes.forEach((k, v) -> clientSessionModel.setNote(k, v));
}
@Override
public String getCurrentRefreshToken() {
return clientSessionModel.getCurrentRefreshToken();
}
@Override
public int getCurrentRefreshTokenUseCount() {
return clientSessionModel.getCurrentRefreshTokenUseCount();
}
@Override
public UUID getId() {
return UUID.fromString(clientSessionModel.getId());
}
@Override
public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
throw new IllegalStateException("not implemented");
}
@Override
public String getUserSessionId() {
return clientSessionModel.getUserSession().getId();
}
@Override
public void setUserSessionId(String userSessionId) {
throw new IllegalStateException("not implemented");
}
};
sessionUpdates.getUpdateTasks().forEach(vSessionUpdateTask -> {
vSessionUpdateTask.runUpdate((V) authenticatedClientSessionEntity);
if (vSessionUpdateTask.getOperation((V) authenticatedClientSessionEntity) == SessionUpdateTask.CacheOperation.REMOVE) {
userSessionPersister.removeClientSession(entity.getUserSessionId(), entity.getClientId(), offline);
}
});
clientSessionModel.getUpdatedModel();
}
}
}
private void processUserSessionUpdate(KeycloakSession innerSession, Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged) {
SessionUpdatesList<V> sessionUpdates = entry.getValue();
SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
RealmModel realm = sessionUpdates.getRealm();
UserSessionPersisterProvider userSessionPersister = innerSession.getProvider(UserSessionPersisterProvider.class);
if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.REMOVE) {
userSessionPersister.removeUserSession(entry.getKey().toString(), offline);
} else if (merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD || merged.getOperation(sessionWrapper.getEntity()) == SessionUpdateTask.CacheOperation.ADD_IF_ABSENT){
UserSessionEntity entity = (UserSessionEntity) sessionWrapper.getEntity();
userSessionPersister.createUserSession(new UserSessionModel() {
@Override
public String getId() {
return entity.getId();
}
@Override
public RealmModel getRealm() {
return new RealmModelDelegate(null) {
@Override
public String getId() {
return entity.getRealmId();
}
};
}
@Override
public String getBrokerSessionId() {
return entity.getBrokerSessionId();
}
@Override
public String getBrokerUserId() {
return entity.getBrokerUserId();
}
@Override
public UserModel getUser() {
return new UserModelDelegate(null) {
@Override
public String getId() {
return entity.getUser();
}
};
}
@Override
public String getLoginUsername() {
return entity.getLoginUsername();
}
@Override
public String getIpAddress() {
return entity.getIpAddress();
}
@Override
public String getAuthMethod() {
return entity.getAuthMethod();
}
@Override
public boolean isRememberMe() {
return entity.isRememberMe();
}
@Override
public int getStarted() {
return entity.getStarted();
}
@Override
public int getLastSessionRefresh() {
return entity.getLastSessionRefresh();
}
@Override
public void setLastSessionRefresh(int seconds) {
throw new IllegalStateException("not implemented");
}
@Override
public boolean isOffline() {
return offline;
}
@Override
public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
// This is not used when saving this to the database.
return Collections.emptyMap();
}
@Override
public void removeAuthenticatedClientSessions(Collection<String> removedClientUUIDS) {
throw new IllegalStateException("not implemented");
}
@Override
public String getNote(String name) {
return entity.getNotes().get(name);
}
@Override
public void setNote(String name, String value) {
throw new IllegalStateException("not implemented");
}
@Override
public void removeNote(String name) {
throw new IllegalStateException("not implemented");
}
@Override
public Map<String, String> getNotes() {
return entity.getNotes();
}
@Override
public State getState() {
return entity.getState();
}
@Override
public void setState(State state) {
throw new IllegalStateException("not implemented");
}
@Override
public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
throw new IllegalStateException("not implemented");
}
}, offline);
} else {
PersistentUserSessionAdapter userSessionModel = (PersistentUserSessionAdapter) userSessionPersister.loadUserSession(realm, entry.getKey().toString(), offline);
if (userSessionModel != null) {
UserSessionEntity userSessionEntity = new UserSessionEntity() {
@Override
public Map<String, String> getNotes() {
return new HashMap<>() {
@Override
public String get(Object key) {
return userSessionModel.getNotes().get(key);
}
@Override
public String put(String key, String value) {
String oldValue = userSessionModel.getNotes().get(key);
userSessionModel.setNote(key, value);
return oldValue;
}
@Override
public String remove(Object key) {
String oldValue = userSessionModel.getNotes().get(key);
userSessionModel.removeNote(key.toString());
return oldValue;
}
@Override
public void clear() {
userSessionModel.getNotes().forEach((k, v) -> userSessionModel.removeNote(k));
}
};
}
@Override
public void setLastSessionRefresh(int lastSessionRefresh) {
userSessionModel.setLastSessionRefresh(lastSessionRefresh);
}
@Override
public void setState(UserSessionModel.State state) {
userSessionModel.setState(state);
}
@Override
public AuthenticatedClientSessionStore getAuthenticatedClientSessions() {
return new AuthenticatedClientSessionStore() {
@Override
public void clear() {
userSessionModel.getAuthenticatedClientSessions().clear();
}
};
}
@Override
public String getRealmId() {
return userSessionModel.getRealm().getId();
}
@Override
public void setRealmId(String realmId) {
userSessionModel.setRealm(innerSession.realms().getRealm(realmId));
}
@Override
public String getId() {
return userSessionModel.getId();
}
@Override
public void setId(String id) {
throw new IllegalStateException("not supported");
}
@Override
public String getUser() {
return userSessionModel.getUser().getId();
}
@Override
public void setUser(String userId) {
userSessionModel.setUser(innerSession.users().getUserById(realm, userId));
}
@Override
public String getLoginUsername() {
return userSessionModel.getLoginUsername();
}
@Override
public void setLoginUsername(String loginUsername) {
userSessionModel.setLoginUsername(loginUsername);
}
@Override
public String getIpAddress() {
return userSessionModel.getIpAddress();
}
@Override
public void setIpAddress(String ipAddress) {
userSessionModel.setIpAddress(ipAddress);
}
@Override
public String getAuthMethod() {
return userSessionModel.getAuthMethod();
}
@Override
public void setAuthMethod(String authMethod) {
userSessionModel.setAuthMethod(authMethod);
}
@Override
public boolean isRememberMe() {
return userSessionModel.isRememberMe();
}
@Override
public void setRememberMe(boolean rememberMe) {
userSessionModel.setRememberMe(rememberMe);
}
@Override
public int getStarted() {
return userSessionModel.getStarted();
}
@Override
public void setStarted(int started) {
userSessionModel.setStarted(started);
}
@Override
public int getLastSessionRefresh() {
return userSessionModel.getLastSessionRefresh();
}
@Override
public void setNotes(Map<String, String> notes) {
userSessionModel.getNotes().keySet().forEach(userSessionModel::removeNote);
notes.forEach((k, v) -> userSessionModel.setNote(k, v));
}
@Override
public void setAuthenticatedClientSessions(AuthenticatedClientSessionStore authenticatedClientSessions) {
throw new IllegalStateException("not supported");
}
@Override
public UserSessionModel.State getState() {
return userSessionModel.getState();
}
@Override
public String getBrokerSessionId() {
return userSessionModel.getBrokerSessionId();
}
@Override
public void setBrokerSessionId(String brokerSessionId) {
userSessionModel.setBrokerSessionId(brokerSessionId);
}
@Override
public String getBrokerUserId() {
return userSessionModel.getBrokerUserId();
}
@Override
public void setBrokerUserId(String brokerUserId) {
userSessionModel.setBrokerUserId(brokerUserId);
}
@Override
public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
throw new IllegalStateException("not supported");
}
};
sessionUpdates.getUpdateTasks().forEach(vSessionUpdateTask -> {
vSessionUpdateTask.runUpdate((V) userSessionEntity);
if (vSessionUpdateTask.getOperation((V)userSessionEntity) == SessionUpdateTask.CacheOperation.REMOVE) {
userSessionPersister.removeUserSession(entry.getKey().toString(), offline);
}
});
userSessionModel.getUpdatedModel();
}
}
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2024 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;
import org.infinispan.Cache;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.SessionFunction;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import java.util.List;
import java.util.Map;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_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> {
private final List<SessionChangesPerformer<K, V>> changesPerformers;
protected final boolean offline;
public PersistentSessionsChangelogBasedTransaction(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader, boolean offline) {
super(session, cache, remoteCacheInvoker, lifespanMsLoader, maxIdleTimeMsLoader);
this.offline = offline;
if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
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))) {
changesPerformers = List.of(
new JpaChangesPerformer<>(session, cache.getName(), offline)
);
} else {
changesPerformers = List.of(
new JpaChangesPerformer<>(session, cache.getName(), offline),
new EmbeddedCachesChangesPerformer<>(cache),
new RemoteCachesChangesPerformer<>(session, cache, remoteCacheInvoker)
);
}
}
@Override
protected void commitImpl() {
for (Map.Entry<K, SessionUpdatesList<V>> entry : updates.entrySet()) {
SessionUpdatesList<V> sessionUpdates = entry.getValue();
SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
// Don't save transient entities to infinispan. They are valid just for current transaction
if (sessionUpdates.getPersistenceState() == UserSessionModel.SessionPersistenceState.TRANSIENT) continue;
RealmModel realm = sessionUpdates.getRealm();
long lifespanMs = lifespanMsLoader.apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity());
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity());
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, lifespanMs, maxIdleTimeMs);
if (merged != null) {
changesPerformers.forEach(p -> p.registerChange(entry, merged));
}
}
changesPerformers.forEach(SessionChangesPerformer::applyChanges);
}
@Override
protected void rollbackImpl() {
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 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;
import org.infinispan.Cache;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class RemoteCachesChangesPerformer<K, V extends SessionEntity> implements SessionChangesPerformer<K, V> {
private final KeycloakSession session;
private final Cache<K, SessionEntityWrapper<V>> cache;
private final RemoteCacheInvoker remoteCacheInvoker;
private final List<Runnable> changes = new LinkedList<>();
public RemoteCachesChangesPerformer(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) {
this.session = session;
this.cache = cache;
this.remoteCacheInvoker = remoteCacheInvoker;
}
@Override
public void registerChange(Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged) {
SessionUpdatesList<V> updates = entry.getValue();
changes.add(() -> remoteCacheInvoker.runTask(session, updates.getRealm(), cache.getName(), entry.getKey(), merged, updates.getEntityWrapper()));
}
@Override
public void applyChanges() {
changes.forEach(Runnable::run);
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 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;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.util.Map;
public interface SessionChangesPerformer<K, V extends SessionEntity> {
void registerChange(Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged);
void applyChanges();
}

View file

@ -0,0 +1,151 @@
/*
* Copyright 2024 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;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider;
import org.keycloak.models.sessions.infinispan.SessionFunction;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import java.util.Collections;
import java.util.Objects;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
public class UserSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction<String, UserSessionEntity> {
private static final Logger LOG = Logger.getLogger(UserSessionPersistentChangelogBasedTransaction.class);
public UserSessionPersistentChangelogBasedTransaction(KeycloakSession session, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, RemoteCacheInvoker remoteCacheInvoker, SessionFunction<UserSessionEntity> lifespanMsLoader, SessionFunction<UserSessionEntity> maxIdleTimeMsLoader, boolean offline) {
super(session, cache, remoteCacheInvoker, lifespanMsLoader, maxIdleTimeMsLoader, offline);
}
public SessionEntityWrapper<UserSessionEntity> get(RealmModel realm, String key) {
SessionUpdatesList<UserSessionEntity> myUpdates = updates.get(key);
if (myUpdates == 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))) {
wrappedEntity = cache.get(key);
}
if (wrappedEntity == null) {
wrappedEntity = getSessionEntityFromPersister(realm, key);
}
if (wrappedEntity == null) {
return null;
}
RealmModel realmFromSession = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealmId());
if (!realmFromSession.getId().equals(realm.getId())) {
LOG.warnf("Realm mismatch for session %s. Expected realm %s, but found realm %s", wrappedEntity.getEntity(), realm.getId(), realmFromSession.getId());
return null;
}
myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
updates.put(key, myUpdates);
return wrappedEntity;
} else {
UserSessionEntity entity = myUpdates.getEntityWrapper().getEntity();
// If entity is scheduled for remove, we don't return it.
boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> {
return task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE;
}).findFirst().isPresent();
return scheduledForRemove ? null : myUpdates.getEntityWrapper();
}
}
public SessionEntityWrapper<UserSessionEntity> getSessionEntityFromPersister(RealmModel realm, String key) {
UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class);
UserSessionModel persistentUserSession = persister.loadUserSession(realm, key, offline);
if (persistentUserSession == null) {
return null;
}
SessionEntityWrapper<UserSessionEntity> userSessionEntitySessionEntityWrapper = importUserSession(persistentUserSession);
if (userSessionEntitySessionEntityWrapper == null) {
removeSessionEntityFromPersister(key);
}
return userSessionEntitySessionEntityWrapper;
}
private void removeSessionEntityFromPersister(String key) {
UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class);
persister.removeUserSession(key, offline);
}
private SessionEntityWrapper<UserSessionEntity> importUserSession(UserSessionModel persistentUserSession) {
String sessionId = persistentUserSession.getId();
if (isScheduledForRemove(sessionId)) {
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))) {
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);
kcSession.sessions().importUserSessions(Collections.singleton(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) {
LOG.debugf("user-session found after import for sessionId=%s offline=%s", sessionId, offline);
return ispnUserSessionEntity;
}
LOG.debugf("user-session could not be found after import for sessionId=%s offline=%s", sessionId, offline);
return null;
}
public boolean isScheduledForRemove(String key) {
return isScheduledForRemove(updates.get(key));
}
private static <V extends SessionEntity> boolean isScheduledForRemove(SessionUpdatesList<V> myUpdates) {
if (myUpdates == null) {
return false;
}
V entity = myUpdates.getEntityWrapper().getEntity();
// If entity is scheduled for remove, we don't return it.
boolean scheduledForRemove = myUpdates.getUpdateTasks()
.stream()
.anyMatch(task -> task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE);
return scheduledForRemove;
}
}

View file

@ -57,6 +57,8 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
private final UUID id;
private transient String userSessionId;
public AuthenticatedClientSessionEntity(UUID id) {
this.id = id;
}
@ -190,6 +192,14 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
return entityWrapper;
}
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
@Override

View file

@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.AuthenticatedClientSessionAdapter;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
@ -165,6 +166,51 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
return true;
}
public String getClient() {
return client;
}
public Predicate<? super UserSessionModel> toModelPredicate() {
return (Predicate<UserSessionModel>) entity -> {
if (!realm.equals(entity.getRealm().getId())) {
return false;
}
if (user != null && !entity.getUser().getId().equals(user)) {
return false;
}
if (client != null && (entity.getAuthenticatedClientSessions() == null || !entity.getAuthenticatedClientSessions().containsKey(client))) {
return false;
}
if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) {
return false;
}
if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) {
return false;
}
if (entity.isRememberMe()) {
if (expiredRememberMe != null && expiredRefreshRememberMe != null && entity.getStarted() > expiredRememberMe && entity.getLastSessionRefresh() > expiredRefreshRememberMe) {
return false;
}
} else {
if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) {
return false;
}
}
if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) {
return false;
}
return true;
};
}
public static class ExternalizerImpl implements Externalizer<UserSessionPredicate> {
private static final int VERSION_1 = 1;

View file

@ -18,10 +18,12 @@
package org.keycloak.models.jpa.session;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@ -38,7 +40,6 @@ import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -81,6 +82,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
entity.setOffline(offlineStr);
entity.setLastSessionRefresh(model.getLastSessionRefresh());
entity.setData(model.getData());
entity.setBrokerSessionId(userSession.getBrokerSessionId());
em.persist(entity);
em.flush();
}
@ -165,7 +167,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
// Remove userSession if it was last clientSession
List<PersistentClientSessionEntity> clientSessions = getClientSessionsByUserSession(sessionEntity.getUserSessionId(), offline);
if (clientSessions.size() == 0) {
if (clientSessions.size() == 0 && offline) {
offlineStr = offlineToString(offline);
PersistentUserSessionEntity userSessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(sessionEntity.getUserSessionId(), offlineStr), LockModeType.PESSIMISTIC_WRITE);
if (userSessionEntity != null) {
@ -252,7 +254,24 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
expiredClientOffline = Time.currentTime() - realm.getClientOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
}
String offlineStr = offlineToString(true);
expire(realm, expiredClientOffline, expiredOffline, true);
if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
int expired = Time.currentTime() - Math.max(realm.getSsoSessionIdleTimeout(), realm.getSsoSessionIdleTimeoutRememberMe()) - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// prefer client session timeout if set
int expiredClient = expired;
if (realm.getClientSessionIdleTimeout() > 0) {
expiredClient = Time.currentTime() - realm.getClientSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
}
expire(realm, expiredClient, expired, false);
}
}
private void expire(RealmModel realm, int expiredClientOffline, int expiredOffline, boolean offline) {
String offlineStr = offlineToString(offline);
logger.tracef("Trigger removing expired user sessions for realm '%s'", realm.getName());
@ -269,7 +288,6 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
.executeUpdate();
logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName());
}
@Override
@ -305,7 +323,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
userSessionQuery.setParameter("userSessionId", userSessionId);
userSessionQuery.setMaxResults(1);
Stream<PersistentUserSessionAdapter> persistentUserSessions = closing(userSessionQuery.getResultStream().map(this::toAdapter));
Stream<OfflineUserSessionModel> persistentUserSessions = closing(userSessionQuery.getResultStream().map(this::toAdapter));
return persistentUserSessions.findAny().map(userSession -> {
@ -330,6 +348,41 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
}).orElse(null);
}
@Override
public UserSessionModel loadUserSessionsStreamByBrokerSessionId(RealmModel realm, String brokerSessionId, boolean offline) {
TypedQuery<PersistentUserSessionEntity> userSessionQuery = em.createNamedQuery("findUserSessionsByBrokerSessionId", PersistentUserSessionEntity.class);
userSessionQuery.setParameter("realmId", realm.getId());
userSessionQuery.setParameter("brokerSessionId", brokerSessionId);
userSessionQuery.setParameter("offline", offlineToString(offline));
userSessionQuery.setMaxResults(1);
Stream<OfflineUserSessionModel> persistentUserSessions = closing(userSessionQuery.getResultStream().map(this::toAdapter));
return persistentUserSessions.findAny().map(userSession -> {
TypedQuery<PersistentClientSessionEntity> clientSessionQuery = em.createNamedQuery("findClientSessionsByUserSession", PersistentClientSessionEntity.class);
clientSessionQuery.setParameter("userSessionId", userSession.getId());
clientSessionQuery.setParameter("offline", offlineToString(userSession.isOffline()));
Set<String> removedClientUUIDs = new HashSet<>();
closing(clientSessionQuery.getResultStream()).forEach(clientSession -> {
boolean added = addClientSessionToAuthenticatedClientSessionsIfPresent(userSession, clientSession);
if (!added) {
// client was removed in the meantime
removedClientUUIDs.add(clientSession.getClientId());
}
}
);
removedClientUUIDs.forEach(this::onClientRemoved);
return userSession;
}).orElse(null);
}
@Override
public Stream<UserSessionModel> loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults) {
@ -417,12 +470,12 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
* @return
*/
private Stream<UserSessionModel> loadUserSessionsWithClientSessions(TypedQuery<PersistentUserSessionEntity> query, String offlineStr, boolean useExact) {
List<PersistentUserSessionAdapter> userSessionAdapters = closing(query.getResultStream()
List<OfflineUserSessionModel> userSessionAdapters = closing(query.getResultStream()
.map(this::toAdapter)
.filter(Objects::nonNull))
.collect(Collectors.toList());
Map<String, PersistentUserSessionAdapter> sessionsById = userSessionAdapters.stream()
Map<String, OfflineUserSessionModel> sessionsById = userSessionAdapters.stream()
.collect(Collectors.toMap(UserSessionModel::getId, Function.identity()));
Set<String> userSessionIds = sessionsById.keySet();
@ -446,7 +499,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
}
closing(queryClientSessions.getResultStream()).forEach(clientSession -> {
PersistentUserSessionAdapter userSession = sessionsById.get(clientSession.getUserSessionId());
OfflineUserSessionModel userSession = sessionsById.get(clientSession.getUserSessionId());
// check if we have a user session for the client session
if (userSession != null) {
boolean added = addClientSessionToAuthenticatedClientSessionsIfPresent(userSession, clientSession);
@ -467,9 +520,9 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
return userSessionAdapters.stream().map(UserSessionModel.class::cast);
}
private boolean addClientSessionToAuthenticatedClientSessionsIfPresent(PersistentUserSessionAdapter userSession, PersistentClientSessionEntity clientSessionEntity) {
private boolean addClientSessionToAuthenticatedClientSessionsIfPresent(OfflineUserSessionModel userSession, PersistentClientSessionEntity clientSessionEntity) {
PersistentAuthenticatedClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSessionEntity);
AuthenticatedClientSessionModel clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSessionEntity);
if (clientSessAdapter.getClient() == null) {
logger.tracef("Not adding client session %s / %s since client is null", userSession, clientSessAdapter);
@ -487,7 +540,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
return true;
}
private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) {
private OfflineUserSessionModel toAdapter(PersistentUserSessionEntity entity) {
RealmModel realm = session.realms().getRealm(entity.getRealmId());
if (realm == null) { // Realm has been deleted concurrently, ignore the entity
return null;
@ -495,19 +548,79 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
return toAdapter(realm, entity);
}
private PersistentUserSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionEntity entity) {
PersistentUserSessionModel model = new PersistentUserSessionModel();
model.setUserSessionId(entity.getUserSessionId());
model.setStarted(entity.getCreatedOn());
model.setLastSessionRefresh(entity.getLastSessionRefresh());
model.setData(entity.getData());
model.setOffline(offlineFromString(entity.getOffline()));
private OfflineUserSessionModel toAdapter(RealmModel realm, PersistentUserSessionEntity entity) {
PersistentUserSessionModel model = new PersistentUserSessionModel() {
@Override
public String getUserSessionId() {
return entity.getUserSessionId();
}
@Override
public void setUserSessionId(String userSessionId) {
entity.setUserSessionId(userSessionId);
}
@Override
public int getStarted() {
return entity.getCreatedOn();
}
@Override
public void setStarted(int started) {
entity.setCreatedOn(started);
}
@Override
public int getLastSessionRefresh() {
return entity.getLastSessionRefresh();
}
@Override
public void setLastSessionRefresh(int lastSessionRefresh) {
entity.setLastSessionRefresh(lastSessionRefresh);
}
@Override
public boolean isOffline() {
return offlineFromString(entity.getOffline());
}
@Override
public void setOffline(boolean offline) {
entity.setOffline(offlineToString(offline));
}
@Override
public String getData() {
return entity.getData();
}
@Override
public void setData(String data) {
entity.setData(data);
}
@Override
public void setRealmId(String realmId) {
entity.setRealmId(realmId);
}
@Override
public void setUserId(String userId) {
entity.setUserId(userId);
}
@Override
public void setBrokerSessionId(String brokerSessionId) {
entity.setBrokerSessionId(brokerSessionId);
}
};
Map<String, AuthenticatedClientSessionModel> clientSessions = new HashMap<>();
return new PersistentUserSessionAdapter(session, model, realm, entity.getUserId(), clientSessions);
}
private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, UserSessionModel userSession, PersistentClientSessionEntity entity) {
private AuthenticatedClientSessionModel toAdapter(RealmModel realm, UserSessionModel userSession, PersistentClientSessionEntity entity) {
String clientId = entity.getClientId();
if (isExternalClient(entity)) {
clientId = getExternalClientId(entity);
@ -515,21 +628,51 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
// can be null if client is not found anymore
ClientModel client = realm.getClientById(clientId);
PersistentClientSessionModel model = new PersistentClientSessionModel();
model.setClientId(clientId);
model.setUserSessionId(userSession.getId());
PersistentClientSessionModel model = new PersistentClientSessionModel() {
@Override
public String getUserSessionId() {
return entity.getUserSessionId();
}
if (userSession instanceof PersistentUserSessionAdapter) {
model.setUserId(((PersistentUserSessionAdapter) userSession).getUserId());
@Override
public void setUserSessionId(String userSessionId) {
entity.setUserSessionId(userSessionId);
}
else {
UserModel user = userSession.getUser();
if (user != null) {
model.setUserId(user.getId());
@Override
public String getClientId() {
String clientId = entity.getClientId();
if (isExternalClient(entity)) {
clientId = getExternalClientId(entity);
}
return clientId;
}
model.setTimestamp(entity.getTimestamp());
model.setData(entity.getData());
@Override
public void setClientId(String clientId) {
throw new IllegalStateException("forbidden");
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public void setTimestamp(int timestamp) {
entity.setTimestamp(timestamp);
}
@Override
public String getData() {
return entity.getData();
}
@Override
public void setData(String data) {
entity.setData(data);
}
};
return new PersistentAuthenticatedClientSessionAdapter(session, model, realm, client, userSession);
}
@ -565,6 +708,19 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
return n.intValue();
}
@Override
public void removeUserSessions(RealmModel realm, boolean offline) {
em.createNamedQuery("deleteClientSessionsByRealmSessionType")
.setParameter("realmId", realm.getId())
.setParameter("offline", offlineToString(offline))
.executeUpdate();
em.createNamedQuery("deleteUserSessionsByRealmSessionType")
.setParameter("realmId", realm.getId())
.setParameter("offline", offlineToString(offline))
.executeUpdate();
}
@Override
public void close() {
// NOOP

View file

@ -24,6 +24,8 @@ import jakarta.persistence.IdClass;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.io.Serializable;
/**
@ -77,6 +79,9 @@ public class PersistentClientSessionEntity {
@Column(name="TIMESTAMP")
protected int timestamp;
@Version
private int version;
@Id
@Column(name = "OFFLINE_FLAG")
protected String offline;

View file

@ -17,6 +17,7 @@
package org.keycloak.models.jpa.session;
import jakarta.persistence.Version;
import org.keycloak.storage.jpa.KeyUtils;
import jakarta.persistence.Column;
@ -33,6 +34,7 @@ import java.io.Serializable;
*/
@NamedQueries({
@NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId"),
@NamedQuery(name="deleteUserSessionsByRealmSessionType", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId and sess.offline = :offline"),
@NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId = :userId"),
@NamedQuery(name="deleteExpiredUserSessions", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId AND sess.offline = :offline AND sess.lastSessionRefresh < :lastSessionRefresh"),
@NamedQuery(name="updateUserSessionLastSessionRefresh", query="update PersistentUserSessionEntity sess set lastSessionRefresh = :lastSessionRefresh where sess.realmId = :realmId" +
@ -45,14 +47,16 @@ import java.io.Serializable;
" AND sess.userSessionId = :userSessionId AND sess.realmId = :realmId"),
@NamedQuery(name="findUserSessionsByUserId", query="select sess from PersistentUserSessionEntity sess where sess.offline = :offline" +
" AND sess.realmId = :realmId AND sess.userId = :userId ORDER BY sess.userSessionId"),
@NamedQuery(name="findUserSessionsByBrokerSessionId", query="select sess from PersistentUserSessionEntity sess where sess.brokerSessionId = :brokerSessionId" +
" AND sess.realmId = :realmId AND sess.offline = :offline ORDER BY sess.userSessionId"),
@NamedQuery(name="findUserSessionsByClientId", query="SELECT sess FROM PersistentUserSessionEntity sess INNER JOIN PersistentClientSessionEntity clientSess " +
" ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientId = :clientId WHERE sess.offline = :offline " +
" ON sess.userSessionId = clientSess.userSessionId AND sess.offline = clientSess.offline AND clientSess.clientId = :clientId WHERE sess.offline = :offline " +
" AND sess.realmId = :realmId ORDER BY sess.userSessionId"),
@NamedQuery(name="findUserSessionsByExternalClientId", query="SELECT sess FROM PersistentUserSessionEntity sess INNER JOIN PersistentClientSessionEntity clientSess " +
" ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientStorageProvider = :clientStorageProvider AND clientSess.externalClientId = :externalClientId WHERE sess.offline = :offline " +
" ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientStorageProvider = :clientStorageProvider AND sess.offline = clientSess.offline AND clientSess.externalClientId = :externalClientId WHERE sess.offline = :offline " +
" AND sess.realmId = :realmId ORDER BY sess.userSessionId"),
@NamedQuery(name="findClientSessionsClientIds", query="SELECT clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider, count(clientSess)" +
" FROM PersistentClientSessionEntity clientSess INNER JOIN PersistentUserSessionEntity sess ON clientSess.userSessionId = sess.userSessionId " +
" FROM PersistentClientSessionEntity clientSess INNER JOIN PersistentUserSessionEntity sess ON clientSess.userSessionId = sess.userSessionId AND sess.offline = clientSess.offline" +
" WHERE sess.offline = :offline AND sess.realmId = :realmId " +
" GROUP BY clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider")
@ -78,6 +82,12 @@ public class PersistentUserSessionEntity {
@Column(name = "LAST_SESSION_REFRESH")
protected int lastSessionRefresh;
@Column(name = "BROKER_SESSION_ID")
protected String brokerSessionId;
@Version
private int version;
@Id
@Column(name = "OFFLINE_FLAG")
protected String offline;
@ -134,6 +144,14 @@ public class PersistentUserSessionEntity {
this.offline = offline;
}
public String getBrokerSessionId() {
return brokerSessionId;
}
public void setBrokerSessionId(String brokerSessionId) {
this.brokerSessionId = brokerSessionId;
}
public String getData() {
return data;
}

View file

@ -18,6 +18,7 @@ package org.keycloak.storage.jpa;
import java.util.regex.Pattern;
import org.jboss.logging.Logger;
import org.keycloak.models.light.LightweightUserAdapter;
/**
*
@ -33,6 +34,8 @@ public class KeyUtils {
UUID_PATTERN.pattern()
+ "|"
+ "f:" + UUID_PATTERN.pattern() + ":.*"
+ "|"
+ LightweightUserAdapter.ID_PREFIX + UUID_PATTERN.pattern()
);
/**

View file

@ -17,6 +17,63 @@
-->
<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">
<addColumn tableName="OFFLINE_USER_SESSION">
<!-- length(broker_session_id) + length(realm_id) <= 1700 for mssql -->
<column name="BROKER_SESSION_ID" type="VARCHAR(1024)" />
<column name="VERSION" type="INT" defaultValueNumeric="0" />
</addColumn>
<addColumn tableName="OFFLINE_CLIENT_SESSION">
<column name="VERSION" type="INT" defaultValueNumeric="0" />
</addColumn>
<createIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_LAST_SESSION_REFRESH" >
<!-- optimize this index for range queries for expire sessions -->
<!-- it should also distribute hot segments across realms and online/offline -->
<column name="REALM_ID" />
<column name="OFFLINE_FLAG" />
<column name="LAST_SESSION_REFRESH" />
</createIndex>
<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_BY_USERSESS" />
<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 author="keycloak" id="25.0.0-xxx-2-mysql">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<or>
<dbms type="mysql"/>
<dbms type="mariadb"/>
</or>
</preConditions>
<createIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_BROKER_SESSION_ID" >
<!-- This is not unique as we can't guarantee if broker sessions are unique across user sessions, and the table might include expired entries.
At least we would need to add the offline flag -->
<column name="BROKER_SESSION_ID(255)" valueComputed="BROKER_SESSION_ID(255)" />
<column name="REALM_ID" />
</createIndex>
</changeSet>
<changeSet author="keycloak" id="25.0.0-xxx-2-not-mysql">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<not>
<or>
<dbms type="mysql"/>
<dbms type="mariadb"/>
</or>
</not>
</preConditions>
<createIndex tableName="OFFLINE_USER_SESSION" indexName="IDX_OFFLINE_USS_BY_BROKER_SESSION_ID" >
<!-- This is not unique as we can't guarantee if broker sessions are unique across user sessions, and the table might include expired entries.
At least we would need to add the offline flag -->
<column name="BROKER_SESSION_ID" />
<column name="REALM_ID" />
</createIndex>
</changeSet>
<changeSet author="keycloak" id="25.0.0-org">
<createTable tableName="ORGANIZATION">
<column name="ID" type="VARCHAR(255)">

View file

@ -5,7 +5,7 @@
# type can be native (for native queries) or jpql (jpql syntax)
# if no type is defined jpql is the default
deleteExpiredClientSessions=delete from PersistentClientSessionEntity sess where sess.userSessionId IN (\
deleteExpiredClientSessions=delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (\
select u.userSessionId from PersistentUserSessionEntity u \
where u.realmId = :realmId AND u.offline = :offline AND u.lastSessionRefresh < :lastSessionRefresh)
@ -13,6 +13,10 @@ deleteClientSessionsByRealm=delete from PersistentClientSessionEntity sess where
select u.userSessionId from PersistentUserSessionEntity u \
where u.realmId = :realmId)
deleteClientSessionsByRealmSessionType=delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (\
select u.userSessionId from PersistentUserSessionEntity u \
where u.realmId = :realmId and u.offline = :offline)
deleteClientSessionsByUser=delete from PersistentClientSessionEntity sess where sess.userSessionId IN (\
select u.userSessionId from PersistentUserSessionEntity u \
where u.userId = :userId)

View file

@ -5,11 +5,14 @@
# if no type is defined jpql is the default
deleteExpiredClientSessions[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = c.OFFLINE_FLAG and u.OFFLINE_FLAG = :offline \
and u.LAST_SESSION_REFRESH < :lastSessionRefresh
deleteClientSessionsByRealm[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId
deleteClientSessionsByRealmSessionType[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.OFFLINE_FLAG = c.OFFLINE_FLAG AND u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline
deleteClientSessionsByUser[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.USER_ID = :userId

View file

@ -5,11 +5,14 @@
# if no type is defined jpql is the default
deleteExpiredClientSessions[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId and u.OFFLINE_FLAG = c.OFFLINE_FLAG and u.OFFLINE_FLAG = :offline \
and u.LAST_SESSION_REFRESH < :lastSessionRefresh
deleteClientSessionsByRealm[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.REALM_ID = :realmId
deleteClientSessionsByRealmSessionType[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.OFFLINE_FLAG = c.OFFLINE_FLAG AND u.REALM_ID = :realmId and u.OFFLINE_FLAG = :offline
deleteClientSessionsByUser[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.USER_ID = :userId

View file

@ -52,9 +52,45 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
data.setNotes(clientSession.getNotes());
data.setRedirectUri(clientSession.getRedirectUri());
model = new PersistentClientSessionModel();
model = new PersistentClientSessionModel() {
private String userSessionId;
private String clientId;
private int timestamp;
private String data;
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
};
model.setClientId(clientSession.getClient().getId());
model.setUserId(clientSession.getUserSession().getUser().getId());
model.setUserSessionId(clientSession.getUserSession().getId());
model.setTimestamp(clientSession.getTimestamp());
@ -151,22 +187,22 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
@Override
public String getCurrentRefreshToken() {
return null; // Information not persisted.
return getData().getCurrentRefreshToken();
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
// Information not persisted.
getData().setCurrentRefreshToken(currentRefreshToken);
}
@Override
public int getCurrentRefreshTokenUseCount() {
return 0; // Information not persisted.
return getData().getCurrentRefreshTokenUseCount();
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
// Information not persisted.
getData().setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override
@ -263,7 +299,10 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
private Set<String> protocolMappers;
@JsonProperty("roles")
private Set<String> roles;
@JsonProperty("currentRefreshToken")
private String currentRefreshToken;
@JsonProperty("currentRefreshTokenUseCount")
private int currentRefreshTokenUseCount;
public String getAuthMethod() {
return authMethod;
@ -336,5 +375,21 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public String getCurrentRefreshToken() {
return currentRefreshToken;
}
public void setCurrentRefreshToken(String currentRefreshToken) {
this.currentRefreshToken = currentRefreshToken;
}
public int getCurrentRefreshTokenUseCount() {
return currentRefreshTokenUseCount;
}
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
this.currentRefreshTokenUseCount = currentRefreshTokenUseCount;
}
}
}

View file

@ -20,52 +20,21 @@ package org.keycloak.models.session;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PersistentClientSessionModel {
public interface PersistentClientSessionModel {
private String userSessionId;
private String clientId;
private String userId;
private int timestamp;
private String data;
String getUserSessionId();
void setUserSessionId(String userSessionId);
public String getUserSessionId() {
return userSessionId;
}
String getClientId();
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
void setClientId(String clientId);
public String getClientId() {
return clientId;
}
int getTimestamp();
public void setClientId(String clientId) {
this.clientId = clientId;
}
void setTimestamp(int timestamp);
public String getUserId() {
return userId;
}
String getData();
public void setUserId(String userId) {
this.userId = userId;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
void setData(String data);
}

View file

@ -18,6 +18,7 @@
package org.keycloak.models.session;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
@ -25,6 +26,7 @@ import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.light.LightweightUserAdapter;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@ -32,6 +34,8 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -40,7 +44,7 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
private final PersistentUserSessionModel model;
private UserModel user;
private String userId;
private final RealmModel realm;
private RealmModel realm;
private KeycloakSession session;
private final Map<String, AuthenticatedClientSessionModel> authenticatedClientSessions;
@ -58,7 +62,78 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
data.setState(other.getState().toString());
}
this.model = new PersistentUserSessionModel();
this.model = new PersistentUserSessionModel() {
private String userSessionId;
private int started;
private int lastSessionRefresh;
private boolean offline;
private String data;
@Override
public String getUserSessionId() {
return userSessionId;
}
@Override
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
@Override
public int getStarted() {
return started;
}
@Override
public void setStarted(int started) {
this.started = started;
}
@Override
public int getLastSessionRefresh() {
return lastSessionRefresh;
}
@Override
public void setLastSessionRefresh(int lastSessionRefresh) {
this.lastSessionRefresh = lastSessionRefresh;
}
@Override
public boolean isOffline() {
return offline;
}
@Override
public void setOffline(boolean offline) {
this.offline = offline;
}
@Override
public String getData() {
return data;
}
@Override
public void setData(String data) {
this.data = data;
}
@Override
public void setRealmId(String realmId) {
/* ignored */
}
@Override
public void setUserId(String userId) {
/* ignored */
}
@Override
public void setBrokerSessionId(String brokerSessionId) {
/* ignored */
}
};
this.model.setStarted(other.getStarted());
this.model.setUserSessionId(other.getId());
this.model.setLastSessionRefresh(other.getLastSessionRefresh());
@ -120,8 +195,12 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
@Override
public UserModel getUser() {
if (user == null) {
if (LightweightUserAdapter.isLightweightUser(userId)) {
user = LightweightUserAdapter.fromString(session, realm, getData().getNotes().get(SESSION_NOTE_LIGHTWEIGHT_USER));
} else {
user = session.users().getUserById(realm, userId);
}
}
return user;
}
@ -137,7 +216,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
@Override
public String getLoginUsername() {
if (isOffline() || !Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
return getUser().getUsername();
} else {
return getData().getLoginUsername();
}
}
@Override
@ -243,6 +326,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
throw new IllegalStateException("Not supported");
}
@Override
public void setLoginUsername(String loginUsername) {
getData().setLoginUsername(loginUsername);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -262,6 +350,41 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
return getId();
}
public void setRealm(RealmModel realm) {
this.realm = realm;
model.setRealmId(realm.getId());
}
public void setUser(UserModel user) {
this.user = user;
model.setUserId(user.getId());
}
public void setIpAddress(String ipAddress) {
getData().setIpAddress(ipAddress);
}
public void setAuthMethod(String authMethod) {
getData().setAuthMethod(authMethod);
}
public void setRememberMe(boolean rememberMe) {
getData().setRememberMe(rememberMe);
}
public void setStarted(int started) {
getData().setStarted(started);
}
public void setBrokerSessionId(String brokerSessionId) {
getData().setBrokerSessionId(brokerSessionId);
model.setBrokerSessionId(brokerSessionId);
}
public void setBrokerUserId(String brokerUserId) {
getData().setBrokerUserId(brokerUserId);
}
protected static class PersistentUserSessionData {
@JsonProperty("brokerSessionId")
@ -289,6 +412,9 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
@JsonProperty("state")
private String state;
@JsonProperty("loginUsername")
private String loginUsername;
public String getBrokerSessionId() {
return brokerSessionId;
}
@ -354,5 +480,13 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
public void setState(String state) {
this.state = state;
}
public void setLoginUsername(String loginUsername) {
this.loginUsername = loginUsername;
}
public String getLoginUsername() {
return loginUsername;
}
}
}

View file

@ -20,51 +20,32 @@ package org.keycloak.models.session;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PersistentUserSessionModel {
public interface PersistentUserSessionModel {
private String userSessionId;
private int started;
private int lastSessionRefresh;
private boolean offline;
private String data;
String getUserSessionId();
public String getUserSessionId() {
return userSessionId;
}
void setUserSessionId(String userSessionId);
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
int getStarted();
public int getStarted() {
return started;
}
void setStarted(int started);
public void setStarted(int started) {
this.started = started;
}
int getLastSessionRefresh();
public int getLastSessionRefresh() {
return lastSessionRefresh;
}
void setLastSessionRefresh(int lastSessionRefresh);
public void setLastSessionRefresh(int lastSessionRefresh) {
this.lastSessionRefresh = lastSessionRefresh;
}
boolean isOffline();
public boolean isOffline() {
return offline;
}
void setOffline(boolean offline);
public void setOffline(boolean offline) {
this.offline = offline;
}
String getData();
public String getData() {
return data;
}
void setData(String data);
void setRealmId(String realmId);
void setUserId(String userId);
void setBrokerSessionId(String brokerSessionId);
public void setData(String data) {
this.data = data;
}
}

View file

@ -86,6 +86,10 @@ public interface UserSessionPersisterProvider extends Provider {
*/
Stream<UserSessionModel> loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults);
default UserSessionModel loadUserSessionsStreamByBrokerSessionId(RealmModel realm, String brokerSessionId, boolean offline) {
throw new IllegalStateException("not implemented");
}
/**
* Called during startup. For each userSession, it loads also clientSessions.
* @param firstResult {@code Integer} Index of the first desired user session. Ignored if negative or {@code null}.
@ -135,4 +139,12 @@ public interface UserSessionPersisterProvider extends Provider {
*/
Map<String, Long> getUserSessionsCountsByClients(RealmModel realm, boolean offline);
/**
* Remove the online user sessions for this realm.
*/
default void removeUserSessions(RealmModel realm, boolean offline) {
throw new IllegalArgumentException("not supported");
// TODO: remove default implementation
}
}

View file

@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.keycloak.common.Profile;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
@ -16,6 +17,9 @@ import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.Start;
import org.keycloak.quarkus.runtime.cli.command.StartDev;
import java.util.Arrays;
import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -26,7 +30,11 @@ import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTI
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FeaturesDistTest {
private static final String PREVIEW_FEATURES_EXPECTED_LOG = "Preview features enabled: admin-fine-grained-authz:v1, client-secret-rotation:v1, dpop:v1, recovery-codes:v1, scripts:v1, token-exchange:v1, update-email:v1";
private static final String PREVIEW_FEATURES_EXPECTED_LOG = "Preview features enabled: " + Arrays.stream(Profile.Feature.values())
.filter(feature -> feature.getType() == Profile.Feature.Type.PREVIEW)
.map(Profile.Feature::getVersionedKey)
.sorted()
.collect(Collectors.joining(", "));
@Test
public void testEnableOnBuild(KeycloakDistribution dist) {
@ -102,6 +110,7 @@ public class FeaturesDistTest {
}
private void assertPreviewFeaturesEnabled(CLIResult result) {
assertThat("expecting at least one preview feature on the list", PREVIEW_FEATURES_EXPECTED_LOG, containsString(":"));
assertThat(result.getOutput(), CoreMatchers.allOf(
containsString(PREVIEW_FEATURES_EXPECTED_LOG)));
}

View file

@ -24,4 +24,6 @@ package org.keycloak.models;
*/
public interface OfflineUserSessionModel extends UserSessionModel {
public String getUserId();
void setLoginUsername(String loginUsername);
}

View file

@ -0,0 +1,134 @@
/*
* Copyright 2024 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.utils;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import java.util.Collection;
import java.util.Map;
/**
* @author Alexander Schwartz
*/
public class UserSessionModelDelegate implements UserSessionModel {
private UserSessionModel delegate;
public UserSessionModelDelegate(UserSessionModel delegate) {
this.delegate = delegate;
}
public String getId() {
return delegate.getId();
}
public RealmModel getRealm() {
return delegate.getRealm();
}
public String getBrokerSessionId() {
return delegate.getBrokerSessionId();
}
public String getBrokerUserId() {
return delegate.getBrokerUserId();
}
public UserModel getUser() {
return delegate.getUser();
}
public String getLoginUsername() {
return delegate.getLoginUsername();
}
public String getIpAddress() {
return delegate.getIpAddress();
}
public String getAuthMethod() {
return delegate.getAuthMethod();
}
public boolean isRememberMe() {
return delegate.isRememberMe();
}
public int getStarted() {
return delegate.getStarted();
}
public int getLastSessionRefresh() {
return delegate.getLastSessionRefresh();
}
public void setLastSessionRefresh(int seconds) {
delegate.setLastSessionRefresh(seconds);
}
public boolean isOffline() {
return delegate.isOffline();
}
public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
return delegate.getAuthenticatedClientSessions();
}
public AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) {
return delegate.getAuthenticatedClientSessionByClient(clientUUID);
}
public void removeAuthenticatedClientSessions(Collection<String> removedClientUUIDS) {
delegate.removeAuthenticatedClientSessions(removedClientUUIDS);
}
public String getNote(String name) {
return delegate.getNote(name);
}
public void setNote(String name, String value) {
delegate.setNote(name, value);
}
public void removeNote(String name) {
delegate.removeNote(name);
}
public Map<String, String> getNotes() {
return delegate.getNotes();
}
public UserSessionModel.State getState() {
return delegate.getState();
}
public void setState(UserSessionModel.State state) {
delegate.setState(state);
}
public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
delegate.restartSession(realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
}
public UserSessionModel.SessionPersistenceState getPersistenceState() {
return delegate.getPersistenceState();
}
}

View file

@ -193,8 +193,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
// only run build during first execution of the server (if the DB is specified), restarts or when running cluster tests
if (restart.get() || "ha".equals(cacheMode) || shouldSetUpDb.get() || configuration.getFipsMode() != FipsMode.DISABLED) {
commands.removeIf("--optimized"::equals);
commands.add("--http-relative-path=/auth");
prepareCommandsForRebuilding(commands);
if ("local".equals(cacheMode)) {
commands.add("--cache=local");
@ -213,6 +212,15 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
return commands;
}
/**
* When enabling automatic rebuilding of the image, the `--optimized` argument must be removed,
* and all original build time parameters must be added.
*/
private static void prepareCommandsForRebuilding(List<String> commands) {
commands.removeIf("--optimized"::equals);
commands.add("--http-relative-path=/auth");
}
protected void addFeaturesOption(List<String> commands) {
String defaultFeatures = configuration.getDefaultFeatures();
@ -238,6 +246,9 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
}
}
// enabling or disabling features requires rebuilding the image
prepareCommandsForRebuilding(commands);
commands.add(featuresOption.toString());
}

View file

@ -304,6 +304,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
String userSessionId = KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
UserSessionModel userSession = createSessions(kcSession)[0];
userSession = kcSession.sessions().getUserSession(realm, userSession.getId());
kcSession.sessions().removeUserSession(realm, userSession);
return userSession.getId();
@ -559,9 +560,11 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
public void testRemovingExpiredSession(KeycloakSession session) {
UserSessionModel[] sessions = createSessions(session);
try {
Time.setOffset(3600000);
UserSessionModel userSession = sessions[0];
RealmModel realm = userSession.getRealm();
// reload userSession in current session
userSession = session.sessions().getUserSession(realm, userSession.getId());
Time.setOffset(3600000);
session.sessions().removeExpired(realm);
// Assert no exception is thrown here

View file

@ -522,6 +522,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
// Refresh with the offline token
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
Assert.assertNull("received error " + tokenResponse.getError() + ", " + tokenResponse.getErrorDescription(), tokenResponse.getError());
// Use accessToken to admin REST request
try (Keycloak offlineTokenAdmin = Keycloak.getInstance(getAuthServerContextRoot() + "/auth",

View file

@ -0,0 +1,22 @@
SessionTest
KcSamlBrokerSessionNotOnOrAfterTest
OidcClaimToUserSessionNoteMapperTest
KcOidcBrokerTransientSessionsTest
KcAdmSessionTest
TransientSessionTest
UserSessionProviderOfflineTest
AuthenticationSessionProviderTest
UserSessionProviderTest
OAuthDanceClientSessionExtensionTest
SessionNotOnOrAfterTest
SessionTimeoutValidationTest
LastSessionRefreshUnitTest
KcOidcUserSessionLimitsBrokerTest
KcSamlUserSessionLimitsBrokerTest
AbstractUserSessionLimitsBrokerTest
UserSessionLimitsTest
ConcurrentLoginTest
RefreshTokenTest
OfflineTokenTest
AccessTokenTest
LogoutTest

View file

@ -212,6 +212,13 @@
</properties>
</profile>
<profile>
<id>jpa+infinispan+persistentsessions</id>
<properties>
<keycloak.model.parameters>Infinispan,Jpa,PersistentUserSessions</keycloak.model.parameters>
</properties>
</profile>
<profile>
<id>jpa+infinispan+client-storage</id>
<properties>

View file

@ -0,0 +1,41 @@
/*
* Copyright 2020 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.testsuite.model.parameters;
import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.testsuite.model.Config;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import java.util.Collections;
public class PersistentUserSessions extends KeycloakModelParameters {
public PersistentUserSessions() {
super(Collections.emptySet(), Collections.emptySet());
}
@Override
public void updateConfig(Config cf) {
updateConfigForJpa(cf);
}
public static void updateConfigForJpa(Config cf) {
System.getProperties().put(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.PERSISTENT_USER_SESSIONS), "enabled");
System.getProperties().put(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE), "enabled");
}
}

View file

@ -29,6 +29,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
@ -258,7 +259,13 @@ public class OfflineSessionPersistenceTest extends KeycloakModelTest {
// Delete local user cache (persisted sessions are still kept)
UserSessionProvider provider = session.getProvider(UserSessionProvider.class);
// Remove in-memory representation of the offline sessions
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true);
} else if (provider instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true);
} else {
throw new IllegalStateException("Unknown UserSessionProvider: " + provider);
}
return null;
});

View file

@ -17,10 +17,13 @@
package org.keycloak.testsuite.model.session;
import org.hamcrest.Matchers;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@ -155,6 +158,9 @@ public class UserSessionInitializerTest extends KeycloakModelTest {
@Test
public void testUserSessionPropagationBetweenSites() throws InterruptedException {
// When user sessions are not stored in the cache, this test doesn't make sense
Assume.assumeThat(Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE), Matchers.not(true));
AtomicInteger index = new AtomicInteger();
AtomicReference<String> userSessionId = new AtomicReference<>();
AtomicReference<List<Boolean>> containsSession = new AtomicReference<>(new LinkedList<>());

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.model.session;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@ -144,6 +145,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
.forEach(userSessionLooper -> persistUserSession(session, userSessionLooper, true));
});
if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
inComittedTransaction(session -> {
// Persist 1 online session
RealmModel realm = session.realms().getRealm(realmId);
@ -156,6 +158,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, false, 1, 1, 1);
assertSession(loadedSessions.get(0), session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
});
}
inComittedTransaction(session -> {
// Assert offline sessions
@ -261,7 +264,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
UserSessionModel userSession = session.sessions().createUserSession(null, fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
userSessionID.set(userSession.getId());
createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
});
inComittedTransaction(session -> {
@ -302,8 +305,8 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
UserSessionModel userSession = session.sessions().createUserSession(null, fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
userSessionID.set(userSession.getId());
createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
createClientSession(session, realmId, fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state");
createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state");
});
inComittedTransaction(session -> {

View file

@ -101,8 +101,8 @@ public class UserSessionProviderModelTest extends KeycloakModelTest {
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
session.sessions().removeUserSession(realm, origSessions[0]);
session.sessions().removeUserSession(realm, origSessions[1]);
session.sessions().removeUserSession(realm, session.sessions().getUserSession(realm, origSessions[0].getId()));
session.sessions().removeUserSession(realm, session.sessions().getUserSession(realm, origSessions[1].getId()));
});
inComittedTransaction(session -> {

View file

@ -24,6 +24,7 @@ import org.infinispan.context.Flag;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
@ -300,10 +301,20 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
session.sessions().createOfflineUserSession(userSession);
session.sessions().createOfflineUserSession(origSessions[0]);
if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
// This does not work with persistent user sessions because we currently have two transactions and the one that creates the offline user sessions is not committing the changes
// try to load user session from persister
Assert.assertEquals(2, persister.loadUserSessionsStream(0, 10, true, "00000000-0000-0000-0000-000000000000").count());
}
});
if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
inComittedTransaction(session -> {
persister = session.getProvider(UserSessionPersisterProvider.class);
Assert.assertEquals(2, persister.loadUserSessionsStream(0, 10, true, "00000000-0000-0000-0000-000000000000").count());
});
}
} finally {
setTimeOffset(0);
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());