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:
parent
757c524cc5
commit
c580c88c93
46 changed files with 4633 additions and 178 deletions
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
|
@ -303,6 +303,63 @@ jobs:
|
||||||
with:
|
with:
|
||||||
job-id: jdk-integration-tests-${{ matrix.os }}-${{ matrix.dist }}-${{ matrix.version }}
|
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:
|
store-integration-tests:
|
||||||
name: Store IT
|
name: Store IT
|
||||||
needs: [build, conditional]
|
needs: [build, conditional]
|
||||||
|
@ -757,6 +814,7 @@ jobs:
|
||||||
- quarkus-integration-tests
|
- quarkus-integration-tests
|
||||||
- jdk-integration-tests
|
- jdk-integration-tests
|
||||||
- store-integration-tests
|
- store-integration-tests
|
||||||
|
- persistent-sessions-tests
|
||||||
- store-model-tests
|
- store-model-tests
|
||||||
- clustering-integration-tests
|
- clustering-integration-tests
|
||||||
- fips-unit-tests
|
- fips-unit-tests
|
||||||
|
|
|
@ -109,6 +109,9 @@ public class Profile {
|
||||||
HOSTNAME_V1("Hostname Options V1", Type.DEFAULT),
|
HOSTNAME_V1("Hostname Options V1", Type.DEFAULT),
|
||||||
//HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2),
|
//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),
|
OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL),
|
||||||
|
|
||||||
DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL),
|
DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL),
|
||||||
|
|
|
@ -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.ClientSessionUpdateTask;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.Tasks;
|
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.changes.sessions.CrossDCLastSessionRefreshChecker;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,14 +43,14 @@ import java.util.UUID;
|
||||||
public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
|
public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
|
||||||
|
|
||||||
private final KeycloakSession kcSession;
|
private final KeycloakSession kcSession;
|
||||||
private final InfinispanUserSessionProvider provider;
|
private final SessionRefreshStore provider;
|
||||||
private AuthenticatedClientSessionEntity entity;
|
private AuthenticatedClientSessionEntity entity;
|
||||||
private final ClientModel client;
|
private final ClientModel client;
|
||||||
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
|
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
|
||||||
private UserSessionModel userSession;
|
private UserSessionModel userSession;
|
||||||
private boolean offline;
|
private boolean offline;
|
||||||
|
|
||||||
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
|
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, SessionRefreshStore provider,
|
||||||
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
|
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
|
||||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
|
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
|
||||||
if (userSession == null) {
|
if (userSession == null) {
|
||||||
|
|
|
@ -90,7 +90,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @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);
|
private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class);
|
||||||
|
|
||||||
|
@ -176,15 +176,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
return offline ? offlineClientSessionTx : clientSessionTx;
|
return offline ? offlineClientSessionTx : clientSessionTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CrossDCLastSessionRefreshStore getLastSessionRefreshStore() {
|
@Override
|
||||||
|
public CrossDCLastSessionRefreshStore getLastSessionRefreshStore() {
|
||||||
return lastSessionRefreshStore;
|
return lastSessionRefreshStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() {
|
@Override
|
||||||
|
public CrossDCLastSessionRefreshStore getOfflineLastSessionRefreshStore() {
|
||||||
return offlineLastSessionRefreshStore;
|
return offlineLastSessionRefreshStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() {
|
@Override
|
||||||
|
public PersisterLastSessionRefreshStore getPersisterLastSessionRefreshStore() {
|
||||||
return persisterLastSessionRefreshStore;
|
return persisterLastSessionRefreshStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,7 +247,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
return adapter;
|
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.setRealmId(realm.getId());
|
||||||
entity.setUser(user.getId());
|
entity.setUser(user.getId());
|
||||||
entity.setLoginUsername(loginUsername);
|
entity.setLoginUsername(loginUsername);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.infinispan.persistence.remote.RemoteStore;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.Environment;
|
import org.keycloak.common.util.Environment;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
|
@ -94,13 +95,29 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||||
private InfinispanKeyGenerator keyGenerator;
|
private InfinispanKeyGenerator keyGenerator;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfinispanUserSessionProvider create(KeycloakSession session) {
|
public UserSessionProvider create(KeycloakSession session) {
|
||||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
|
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<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>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
|
||||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_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(
|
return new InfinispanUserSessionProvider(
|
||||||
session,
|
session,
|
||||||
remoteCacheInvoker,
|
remoteCacheInvoker,
|
||||||
|
@ -148,8 +165,15 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||||
} else if (event instanceof UserModel.UserRemovedEvent) {
|
} else if (event instanceof UserModel.UserRemovedEvent) {
|
||||||
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
|
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
|
||||||
|
|
||||||
InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
|
UserSessionProvider provider1 = userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
|
||||||
provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
|
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) {
|
} else if (event instanceof ResetTimeOffsetEvent) {
|
||||||
if (persisterLastSessionRefreshStore != null) {
|
if (persisterLastSessionRefreshStore != null) {
|
||||||
persisterLastSessionRefreshStore.reset();
|
persisterLastSessionRefreshStore.reset();
|
||||||
|
@ -212,6 +236,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||||
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
|
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
|
||||||
if (provider instanceof InfinispanUserSessionProvider) {
|
if (provider instanceof InfinispanUserSessionProvider) {
|
||||||
((InfinispanUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId());
|
((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) {
|
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
|
||||||
if (provider instanceof InfinispanUserSessionProvider) {
|
if (provider instanceof InfinispanUserSessionProvider) {
|
||||||
((InfinispanUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
|
((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) {
|
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
|
||||||
if (provider instanceof InfinispanUserSessionProvider) {
|
if (provider instanceof InfinispanUserSessionProvider) {
|
||||||
((InfinispanUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId());
|
((InfinispanUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId());
|
||||||
|
} else if (provider instanceof PersistentUserSessionProvider) {
|
||||||
|
((PersistentUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
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.InfinispanChangelogBasedTransaction;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshChecker;
|
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshChecker;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||||
|
@ -43,16 +44,14 @@ import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
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>
|
* @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 KeycloakSession session;
|
||||||
|
|
||||||
private final InfinispanUserSessionProvider provider;
|
private final T provider;
|
||||||
|
|
||||||
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
|
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ public class UserSessionAdapter implements UserSessionModel {
|
||||||
|
|
||||||
private SessionPersistenceState persistenceState;
|
private SessionPersistenceState persistenceState;
|
||||||
|
|
||||||
public UserSessionAdapter(KeycloakSession session, UserModel user, InfinispanUserSessionProvider provider,
|
public UserSessionAdapter(KeycloakSession session, UserModel user, T provider,
|
||||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
|
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
|
||||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
|
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
|
||||||
RealmModel realm, UserSessionEntity entity, boolean offline) {
|
RealmModel realm, UserSessionEntity entity, boolean offline) {
|
||||||
|
@ -94,7 +93,7 @@ public class UserSessionAdapter implements UserSessionModel {
|
||||||
// Check if client still exists
|
// Check if client still exists
|
||||||
ClientModel client = realm.getClientById(key);
|
ClientModel client = realm.getClientById(key);
|
||||||
if (client != null) {
|
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) {
|
if (clientSession != null) {
|
||||||
result.put(key, clientSession);
|
result.put(key, clientSession);
|
||||||
}
|
}
|
||||||
|
@ -330,7 +329,7 @@ public class UserSessionAdapter implements UserSessionModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void runUpdate(UserSessionEntity entity) {
|
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.setState(null);
|
||||||
entity.getNotes().clear();
|
entity.getNotes().clear();
|
||||||
|
@ -360,7 +359,8 @@ public class UserSessionAdapter implements UserSessionModel {
|
||||||
return getId().hashCode();
|
return getId().hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSessionEntity getEntity() {
|
// TODO: This should not be public
|
||||||
|
public UserSessionEntity getEntity() {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,11 +19,13 @@ package org.keycloak.models.sessions.infinispan.changes;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.context.Flag;
|
import org.infinispan.context.Flag;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.models.AbstractKeycloakTransaction;
|
import org.keycloak.models.AbstractKeycloakTransaction;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
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.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
|
||||||
import org.keycloak.connections.infinispan.InfinispanUtil;
|
import org.keycloak.connections.infinispan.InfinispanUtil;
|
||||||
|
|
||||||
|
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
|
||||||
|
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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);
|
public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
|
||||||
|
|
||||||
private final KeycloakSession kcSession;
|
protected final KeycloakSession kcSession;
|
||||||
private final String cacheName;
|
private final String cacheName;
|
||||||
private final Cache<K, SessionEntityWrapper<V>> cache;
|
protected final Cache<K, SessionEntityWrapper<V>> cache;
|
||||||
private final RemoteCacheInvoker remoteCacheInvoker;
|
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;
|
protected final SessionFunction<V> lifespanMsLoader;
|
||||||
private final SessionFunction<V> maxIdleTimeMsLoader;
|
protected final SessionFunction<V> maxIdleTimeMsLoader;
|
||||||
|
|
||||||
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker,
|
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker,
|
||||||
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
|
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) {
|
public void addTask(K key, SessionUpdateTask<V> task) {
|
||||||
SessionUpdatesList<V> myUpdates = updates.get(key);
|
SessionUpdatesList<V> myUpdates = updates.get(key);
|
||||||
if (myUpdates == null) {
|
if (myUpdates == null) {
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && (Objects.equals(cacheName, USER_SESSION_CACHE_NAME) || Objects.equals(cacheName, CLIENT_SESSION_CACHE_NAME))) {
|
||||||
|
throw new IllegalStateException("Can't load from cache");
|
||||||
|
}
|
||||||
|
|
||||||
// Lookup entity from cache
|
// Lookup entity from cache
|
||||||
SessionEntityWrapper<V> wrappedEntity = cache.get(key);
|
SessionEntityWrapper<V> wrappedEntity = cache.get(key);
|
||||||
if (wrappedEntity == null) {
|
if (wrappedEntity == null) {
|
||||||
|
@ -95,10 +104,12 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity, persistenceState);
|
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity, persistenceState);
|
||||||
updates.put(key, myUpdates);
|
updates.put(key, myUpdates);
|
||||||
|
|
||||||
|
if (task != null) {
|
||||||
// Run the update now, so reader in same transaction can see it
|
// Run the update now, so reader in same transaction can see it
|
||||||
task.runUpdate(entity);
|
task.runUpdate(entity);
|
||||||
myUpdates.add(task);
|
myUpdates.add(task);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void reloadEntityInCurrentTransaction(RealmModel realm, K key, SessionEntityWrapper<V> entity) {
|
public void reloadEntityInCurrentTransaction(RealmModel realm, K key, SessionEntityWrapper<V> entity) {
|
||||||
|
@ -150,7 +161,6 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void commitImpl() {
|
protected void commitImpl() {
|
||||||
for (Map.Entry<K, SessionUpdatesList<V>> entry : updates.entrySet()) {
|
for (Map.Entry<K, SessionUpdatesList<V>> entry : updates.entrySet()) {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -57,6 +57,8 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
|
||||||
|
|
||||||
private final UUID id;
|
private final UUID id;
|
||||||
|
|
||||||
|
private transient String userSessionId;
|
||||||
|
|
||||||
public AuthenticatedClientSessionEntity(UUID id) {
|
public AuthenticatedClientSessionEntity(UUID id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
@ -190,6 +192,14 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
|
||||||
return entityWrapper;
|
return entityWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserSessionId() {
|
||||||
|
return userSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserSessionId(String userSessionId) {
|
||||||
|
this.userSessionId = userSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
|
public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.models.sessions.infinispan.stream;
|
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.AuthenticatedClientSessionAdapter;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||||
|
@ -165,6 +166,51 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
|
||||||
return true;
|
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> {
|
public static class ExternalizerImpl implements Externalizer<UserSessionPredicate> {
|
||||||
|
|
||||||
private static final int VERSION_1 = 1;
|
private static final int VERSION_1 = 1;
|
||||||
|
|
|
@ -18,10 +18,12 @@
|
||||||
package org.keycloak.models.jpa.session;
|
package org.keycloak.models.jpa.session;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OfflineUserSessionModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
@ -38,7 +40,6 @@ import jakarta.persistence.Query;
|
||||||
import jakarta.persistence.TypedQuery;
|
import jakarta.persistence.TypedQuery;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -81,6 +82,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
entity.setOffline(offlineStr);
|
entity.setOffline(offlineStr);
|
||||||
entity.setLastSessionRefresh(model.getLastSessionRefresh());
|
entity.setLastSessionRefresh(model.getLastSessionRefresh());
|
||||||
entity.setData(model.getData());
|
entity.setData(model.getData());
|
||||||
|
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
||||||
em.persist(entity);
|
em.persist(entity);
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
@ -165,7 +167,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
|
|
||||||
// Remove userSession if it was last clientSession
|
// Remove userSession if it was last clientSession
|
||||||
List<PersistentClientSessionEntity> clientSessions = getClientSessionsByUserSession(sessionEntity.getUserSessionId(), offline);
|
List<PersistentClientSessionEntity> clientSessions = getClientSessionsByUserSession(sessionEntity.getUserSessionId(), offline);
|
||||||
if (clientSessions.size() == 0) {
|
if (clientSessions.size() == 0 && offline) {
|
||||||
offlineStr = offlineToString(offline);
|
offlineStr = offlineToString(offline);
|
||||||
PersistentUserSessionEntity userSessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(sessionEntity.getUserSessionId(), offlineStr), LockModeType.PESSIMISTIC_WRITE);
|
PersistentUserSessionEntity userSessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(sessionEntity.getUserSessionId(), offlineStr), LockModeType.PESSIMISTIC_WRITE);
|
||||||
if (userSessionEntity != null) {
|
if (userSessionEntity != null) {
|
||||||
|
@ -252,7 +254,24 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
expiredClientOffline = Time.currentTime() - realm.getClientOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
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());
|
logger.tracef("Trigger removing expired user sessions for realm '%s'", realm.getName());
|
||||||
|
|
||||||
|
@ -269,7 +288,6 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
.executeUpdate();
|
.executeUpdate();
|
||||||
|
|
||||||
logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName());
|
logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -305,7 +323,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
userSessionQuery.setParameter("userSessionId", userSessionId);
|
userSessionQuery.setParameter("userSessionId", userSessionId);
|
||||||
userSessionQuery.setMaxResults(1);
|
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 -> {
|
return persistentUserSessions.findAny().map(userSession -> {
|
||||||
|
|
||||||
|
@ -330,6 +348,41 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
}).orElse(null);
|
}).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
|
@Override
|
||||||
public Stream<UserSessionModel> loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults) {
|
public Stream<UserSessionModel> loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults) {
|
||||||
|
|
||||||
|
@ -417,12 +470,12 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private Stream<UserSessionModel> loadUserSessionsWithClientSessions(TypedQuery<PersistentUserSessionEntity> query, String offlineStr, boolean useExact) {
|
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)
|
.map(this::toAdapter)
|
||||||
.filter(Objects::nonNull))
|
.filter(Objects::nonNull))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Map<String, PersistentUserSessionAdapter> sessionsById = userSessionAdapters.stream()
|
Map<String, OfflineUserSessionModel> sessionsById = userSessionAdapters.stream()
|
||||||
.collect(Collectors.toMap(UserSessionModel::getId, Function.identity()));
|
.collect(Collectors.toMap(UserSessionModel::getId, Function.identity()));
|
||||||
|
|
||||||
Set<String> userSessionIds = sessionsById.keySet();
|
Set<String> userSessionIds = sessionsById.keySet();
|
||||||
|
@ -446,7 +499,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
}
|
}
|
||||||
|
|
||||||
closing(queryClientSessions.getResultStream()).forEach(clientSession -> {
|
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
|
// check if we have a user session for the client session
|
||||||
if (userSession != null) {
|
if (userSession != null) {
|
||||||
boolean added = addClientSessionToAuthenticatedClientSessionsIfPresent(userSession, clientSession);
|
boolean added = addClientSessionToAuthenticatedClientSessionsIfPresent(userSession, clientSession);
|
||||||
|
@ -467,9 +520,9 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
return userSessionAdapters.stream().map(UserSessionModel.class::cast);
|
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) {
|
if (clientSessAdapter.getClient() == null) {
|
||||||
logger.tracef("Not adding client session %s / %s since client is null", userSession, clientSessAdapter);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) {
|
private OfflineUserSessionModel toAdapter(PersistentUserSessionEntity entity) {
|
||||||
RealmModel realm = session.realms().getRealm(entity.getRealmId());
|
RealmModel realm = session.realms().getRealm(entity.getRealmId());
|
||||||
if (realm == null) { // Realm has been deleted concurrently, ignore the entity
|
if (realm == null) { // Realm has been deleted concurrently, ignore the entity
|
||||||
return null;
|
return null;
|
||||||
|
@ -495,19 +548,79 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
return toAdapter(realm, entity);
|
return toAdapter(realm, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PersistentUserSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionEntity entity) {
|
private OfflineUserSessionModel toAdapter(RealmModel realm, PersistentUserSessionEntity entity) {
|
||||||
PersistentUserSessionModel model = new PersistentUserSessionModel();
|
PersistentUserSessionModel model = new PersistentUserSessionModel() {
|
||||||
model.setUserSessionId(entity.getUserSessionId());
|
@Override
|
||||||
model.setStarted(entity.getCreatedOn());
|
public String getUserSessionId() {
|
||||||
model.setLastSessionRefresh(entity.getLastSessionRefresh());
|
return entity.getUserSessionId();
|
||||||
model.setData(entity.getData());
|
}
|
||||||
model.setOffline(offlineFromString(entity.getOffline()));
|
|
||||||
|
@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<>();
|
Map<String, AuthenticatedClientSessionModel> clientSessions = new HashMap<>();
|
||||||
return new PersistentUserSessionAdapter(session, model, realm, entity.getUserId(), clientSessions);
|
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();
|
String clientId = entity.getClientId();
|
||||||
if (isExternalClient(entity)) {
|
if (isExternalClient(entity)) {
|
||||||
clientId = getExternalClientId(entity);
|
clientId = getExternalClientId(entity);
|
||||||
|
@ -515,21 +628,51 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
// can be null if client is not found anymore
|
// can be null if client is not found anymore
|
||||||
ClientModel client = realm.getClientById(clientId);
|
ClientModel client = realm.getClientById(clientId);
|
||||||
|
|
||||||
PersistentClientSessionModel model = new PersistentClientSessionModel();
|
PersistentClientSessionModel model = new PersistentClientSessionModel() {
|
||||||
model.setClientId(clientId);
|
@Override
|
||||||
model.setUserSessionId(userSession.getId());
|
public String getUserSessionId() {
|
||||||
|
return entity.getUserSessionId();
|
||||||
|
}
|
||||||
|
|
||||||
if (userSession instanceof PersistentUserSessionAdapter) {
|
@Override
|
||||||
model.setUserId(((PersistentUserSessionAdapter) userSession).getUserId());
|
public void setUserSessionId(String userSessionId) {
|
||||||
|
entity.setUserSessionId(userSessionId);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
UserModel user = userSession.getUser();
|
@Override
|
||||||
if (user != null) {
|
public String getClientId() {
|
||||||
model.setUserId(user.getId());
|
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);
|
return new PersistentAuthenticatedClientSessionAdapter(session, model, realm, client, userSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,6 +708,19 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
return n.intValue();
|
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
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
// NOOP
|
// NOOP
|
||||||
|
|
|
@ -24,6 +24,8 @@ import jakarta.persistence.IdClass;
|
||||||
import jakarta.persistence.NamedQueries;
|
import jakarta.persistence.NamedQueries;
|
||||||
import jakarta.persistence.NamedQuery;
|
import jakarta.persistence.NamedQuery;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +79,9 @@ public class PersistentClientSessionEntity {
|
||||||
@Column(name="TIMESTAMP")
|
@Column(name="TIMESTAMP")
|
||||||
protected int timestamp;
|
protected int timestamp;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private int version;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "OFFLINE_FLAG")
|
@Column(name = "OFFLINE_FLAG")
|
||||||
protected String offline;
|
protected String offline;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.models.jpa.session;
|
package org.keycloak.models.jpa.session;
|
||||||
|
|
||||||
|
import jakarta.persistence.Version;
|
||||||
import org.keycloak.storage.jpa.KeyUtils;
|
import org.keycloak.storage.jpa.KeyUtils;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
|
@ -33,6 +34,7 @@ import java.io.Serializable;
|
||||||
*/
|
*/
|
||||||
@NamedQueries({
|
@NamedQueries({
|
||||||
@NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId"),
|
@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="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="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" +
|
@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"),
|
" AND sess.userSessionId = :userSessionId AND sess.realmId = :realmId"),
|
||||||
@NamedQuery(name="findUserSessionsByUserId", query="select sess from PersistentUserSessionEntity sess where sess.offline = :offline" +
|
@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"),
|
" 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 " +
|
@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"),
|
" AND sess.realmId = :realmId ORDER BY sess.userSessionId"),
|
||||||
@NamedQuery(name="findUserSessionsByExternalClientId", query="SELECT sess FROM PersistentUserSessionEntity sess INNER JOIN PersistentClientSessionEntity clientSess " +
|
@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"),
|
" AND sess.realmId = :realmId ORDER BY sess.userSessionId"),
|
||||||
@NamedQuery(name="findClientSessionsClientIds", query="SELECT clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider, count(clientSess)" +
|
@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 " +
|
" WHERE sess.offline = :offline AND sess.realmId = :realmId " +
|
||||||
" GROUP BY clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider")
|
" GROUP BY clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider")
|
||||||
|
|
||||||
|
@ -78,6 +82,12 @@ public class PersistentUserSessionEntity {
|
||||||
@Column(name = "LAST_SESSION_REFRESH")
|
@Column(name = "LAST_SESSION_REFRESH")
|
||||||
protected int lastSessionRefresh;
|
protected int lastSessionRefresh;
|
||||||
|
|
||||||
|
@Column(name = "BROKER_SESSION_ID")
|
||||||
|
protected String brokerSessionId;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private int version;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "OFFLINE_FLAG")
|
@Column(name = "OFFLINE_FLAG")
|
||||||
protected String offline;
|
protected String offline;
|
||||||
|
@ -134,6 +144,14 @@ public class PersistentUserSessionEntity {
|
||||||
this.offline = offline;
|
this.offline = offline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBrokerSessionId() {
|
||||||
|
return brokerSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrokerSessionId(String brokerSessionId) {
|
||||||
|
this.brokerSessionId = brokerSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getData() {
|
public String getData() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.storage.jpa;
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.light.LightweightUserAdapter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -33,6 +34,8 @@ public class KeyUtils {
|
||||||
UUID_PATTERN.pattern()
|
UUID_PATTERN.pattern()
|
||||||
+ "|"
|
+ "|"
|
||||||
+ "f:" + UUID_PATTERN.pattern() + ":.*"
|
+ "f:" + UUID_PATTERN.pattern() + ":.*"
|
||||||
|
+ "|"
|
||||||
|
+ LightweightUserAdapter.ID_PREFIX + UUID_PATTERN.pattern()
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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">
|
<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">
|
<changeSet author="keycloak" id="25.0.0-org">
|
||||||
<createTable tableName="ORGANIZATION">
|
<createTable tableName="ORGANIZATION">
|
||||||
<column name="ID" type="VARCHAR(255)">
|
<column name="ID" type="VARCHAR(255)">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
# type can be native (for native queries) or jpql (jpql syntax)
|
# type can be native (for native queries) or jpql (jpql syntax)
|
||||||
# if no type is defined jpql is the default
|
# 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 \
|
select u.userSessionId from PersistentUserSessionEntity u \
|
||||||
where u.realmId = :realmId AND u.offline = :offline AND u.lastSessionRefresh < :lastSessionRefresh)
|
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 \
|
select u.userSessionId from PersistentUserSessionEntity u \
|
||||||
where u.realmId = :realmId)
|
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 (\
|
deleteClientSessionsByUser=delete from PersistentClientSessionEntity sess where sess.userSessionId IN (\
|
||||||
select u.userSessionId from PersistentUserSessionEntity u \
|
select u.userSessionId from PersistentUserSessionEntity u \
|
||||||
where u.userId = :userId)
|
where u.userId = :userId)
|
||||||
|
|
|
@ -5,11 +5,14 @@
|
||||||
# if no type is defined jpql is the default
|
# if no type is defined jpql is the default
|
||||||
|
|
||||||
deleteExpiredClientSessions[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
|
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
|
and u.LAST_SESSION_REFRESH < :lastSessionRefresh
|
||||||
|
|
||||||
deleteClientSessionsByRealm[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
|
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
|
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 \
|
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
|
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.USER_ID = :userId
|
||||||
|
|
|
@ -5,11 +5,14 @@
|
||||||
# if no type is defined jpql is the default
|
# if no type is defined jpql is the default
|
||||||
|
|
||||||
deleteExpiredClientSessions[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
|
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
|
and u.LAST_SESSION_REFRESH < :lastSessionRefresh
|
||||||
|
|
||||||
deleteClientSessionsByRealm[native]=delete c from OFFLINE_CLIENT_SESSION c join OFFLINE_USER_SESSION u \
|
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
|
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 \
|
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
|
where c.USER_SESSION_ID = u.USER_SESSION_ID and u.USER_ID = :userId
|
||||||
|
|
|
@ -52,9 +52,45 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
|
||||||
data.setNotes(clientSession.getNotes());
|
data.setNotes(clientSession.getNotes());
|
||||||
data.setRedirectUri(clientSession.getRedirectUri());
|
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.setClientId(clientSession.getClient().getId());
|
||||||
model.setUserId(clientSession.getUserSession().getUser().getId());
|
|
||||||
model.setUserSessionId(clientSession.getUserSession().getId());
|
model.setUserSessionId(clientSession.getUserSession().getId());
|
||||||
model.setTimestamp(clientSession.getTimestamp());
|
model.setTimestamp(clientSession.getTimestamp());
|
||||||
|
|
||||||
|
@ -151,22 +187,22 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCurrentRefreshToken() {
|
public String getCurrentRefreshToken() {
|
||||||
return null; // Information not persisted.
|
return getData().getCurrentRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setCurrentRefreshToken(String currentRefreshToken) {
|
public void setCurrentRefreshToken(String currentRefreshToken) {
|
||||||
// Information not persisted.
|
getData().setCurrentRefreshToken(currentRefreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCurrentRefreshTokenUseCount() {
|
public int getCurrentRefreshTokenUseCount() {
|
||||||
return 0; // Information not persisted.
|
return getData().getCurrentRefreshTokenUseCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
|
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
|
||||||
// Information not persisted.
|
getData().setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -263,7 +299,10 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
|
||||||
private Set<String> protocolMappers;
|
private Set<String> protocolMappers;
|
||||||
@JsonProperty("roles")
|
@JsonProperty("roles")
|
||||||
private Set<String> roles;
|
private Set<String> roles;
|
||||||
|
@JsonProperty("currentRefreshToken")
|
||||||
|
private String currentRefreshToken;
|
||||||
|
@JsonProperty("currentRefreshTokenUseCount")
|
||||||
|
private int currentRefreshTokenUseCount;
|
||||||
|
|
||||||
public String getAuthMethod() {
|
public String getAuthMethod() {
|
||||||
return authMethod;
|
return authMethod;
|
||||||
|
@ -336,5 +375,21 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
|
||||||
public void setRoles(Set<String> roles) {
|
public void setRoles(Set<String> roles) {
|
||||||
this.roles = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,52 +20,21 @@ package org.keycloak.models.session;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class PersistentClientSessionModel {
|
public interface PersistentClientSessionModel {
|
||||||
|
|
||||||
private String userSessionId;
|
String getUserSessionId();
|
||||||
private String clientId;
|
|
||||||
private String userId;
|
|
||||||
private int timestamp;
|
|
||||||
private String data;
|
|
||||||
|
|
||||||
|
void setUserSessionId(String userSessionId);
|
||||||
|
|
||||||
public String getUserSessionId() {
|
String getClientId();
|
||||||
return userSessionId;
|
|
||||||
}
|
void setClientId(String clientId);
|
||||||
|
|
||||||
public void setUserSessionId(String userSessionId) {
|
int getTimestamp();
|
||||||
this.userSessionId = userSessionId;
|
|
||||||
}
|
void setTimestamp(int timestamp);
|
||||||
|
|
||||||
public String getClientId() {
|
String getData();
|
||||||
return clientId;
|
|
||||||
}
|
void setData(String data);
|
||||||
|
|
||||||
public void setClientId(String clientId) {
|
|
||||||
this.clientId = clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.models.session;
|
package org.keycloak.models.session;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
|
@ -25,6 +26,7 @@ import org.keycloak.models.OfflineUserSessionModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.light.LightweightUserAdapter;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -32,6 +34,8 @@ import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
|
@ -40,7 +44,7 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
private final PersistentUserSessionModel model;
|
private final PersistentUserSessionModel model;
|
||||||
private UserModel user;
|
private UserModel user;
|
||||||
private String userId;
|
private String userId;
|
||||||
private final RealmModel realm;
|
private RealmModel realm;
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
private final Map<String, AuthenticatedClientSessionModel> authenticatedClientSessions;
|
private final Map<String, AuthenticatedClientSessionModel> authenticatedClientSessions;
|
||||||
|
|
||||||
|
@ -58,7 +62,78 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
data.setState(other.getState().toString());
|
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.setStarted(other.getStarted());
|
||||||
this.model.setUserSessionId(other.getId());
|
this.model.setUserSessionId(other.getId());
|
||||||
this.model.setLastSessionRefresh(other.getLastSessionRefresh());
|
this.model.setLastSessionRefresh(other.getLastSessionRefresh());
|
||||||
|
@ -120,8 +195,12 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
@Override
|
@Override
|
||||||
public UserModel getUser() {
|
public UserModel getUser() {
|
||||||
if (user == null) {
|
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);
|
user = session.users().getUserById(realm, userId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +216,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getLoginUsername() {
|
public String getLoginUsername() {
|
||||||
|
if (isOffline() || !Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
|
||||||
return getUser().getUsername();
|
return getUser().getUsername();
|
||||||
|
} else {
|
||||||
|
return getData().getLoginUsername();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -243,6 +326,11 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
throw new IllegalStateException("Not supported");
|
throw new IllegalStateException("Not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLoginUsername(String loginUsername) {
|
||||||
|
getData().setLoginUsername(loginUsername);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
@ -262,6 +350,41 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
return getId();
|
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 {
|
protected static class PersistentUserSessionData {
|
||||||
|
|
||||||
@JsonProperty("brokerSessionId")
|
@JsonProperty("brokerSessionId")
|
||||||
|
@ -289,6 +412,9 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
@JsonProperty("state")
|
@JsonProperty("state")
|
||||||
private String state;
|
private String state;
|
||||||
|
|
||||||
|
@JsonProperty("loginUsername")
|
||||||
|
private String loginUsername;
|
||||||
|
|
||||||
public String getBrokerSessionId() {
|
public String getBrokerSessionId() {
|
||||||
return brokerSessionId;
|
return brokerSessionId;
|
||||||
}
|
}
|
||||||
|
@ -354,5 +480,13 @@ public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
|
||||||
public void setState(String state) {
|
public void setState(String state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setLoginUsername(String loginUsername) {
|
||||||
|
this.loginUsername = loginUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoginUsername() {
|
||||||
|
return loginUsername;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,51 +20,32 @@ package org.keycloak.models.session;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class PersistentUserSessionModel {
|
public interface PersistentUserSessionModel {
|
||||||
|
|
||||||
private String userSessionId;
|
String getUserSessionId();
|
||||||
private int started;
|
|
||||||
private int lastSessionRefresh;
|
|
||||||
private boolean offline;
|
|
||||||
private String data;
|
|
||||||
|
|
||||||
public String getUserSessionId() {
|
void setUserSessionId(String userSessionId);
|
||||||
return userSessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserSessionId(String userSessionId) {
|
int getStarted();
|
||||||
this.userSessionId = userSessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getStarted() {
|
void setStarted(int started);
|
||||||
return started;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStarted(int started) {
|
int getLastSessionRefresh();
|
||||||
this.started = started;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLastSessionRefresh() {
|
void setLastSessionRefresh(int lastSessionRefresh);
|
||||||
return lastSessionRefresh;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastSessionRefresh(int lastSessionRefresh) {
|
boolean isOffline();
|
||||||
this.lastSessionRefresh = lastSessionRefresh;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOffline() {
|
void setOffline(boolean offline);
|
||||||
return offline;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOffline(boolean offline) {
|
String getData();
|
||||||
this.offline = offline;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getData() {
|
void setData(String data);
|
||||||
return data;
|
|
||||||
}
|
void setRealmId(String realmId);
|
||||||
|
|
||||||
|
void setUserId(String userId);
|
||||||
|
|
||||||
|
void setBrokerSessionId(String brokerSessionId);
|
||||||
|
|
||||||
public void setData(String data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,10 @@ public interface UserSessionPersisterProvider extends Provider {
|
||||||
*/
|
*/
|
||||||
Stream<UserSessionModel> loadUserSessionsStream(RealmModel realm, ClientModel client, boolean offline, Integer firstResult, Integer maxResults);
|
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.
|
* 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}.
|
* @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);
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.TestMethodOrder;
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||||
import org.junit.jupiter.api.condition.OS;
|
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.CLIResult;
|
||||||
import org.keycloak.it.junit5.extension.DistributionTest;
|
import org.keycloak.it.junit5.extension.DistributionTest;
|
||||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
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.Start;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.StartDev;
|
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.CoreMatchers.containsString;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
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)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
public class FeaturesDistTest {
|
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
|
@Test
|
||||||
public void testEnableOnBuild(KeycloakDistribution dist) {
|
public void testEnableOnBuild(KeycloakDistribution dist) {
|
||||||
|
@ -102,6 +110,7 @@ public class FeaturesDistTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertPreviewFeaturesEnabled(CLIResult result) {
|
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(
|
assertThat(result.getOutput(), CoreMatchers.allOf(
|
||||||
containsString(PREVIEW_FEATURES_EXPECTED_LOG)));
|
containsString(PREVIEW_FEATURES_EXPECTED_LOG)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,4 +24,6 @@ package org.keycloak.models;
|
||||||
*/
|
*/
|
||||||
public interface OfflineUserSessionModel extends UserSessionModel {
|
public interface OfflineUserSessionModel extends UserSessionModel {
|
||||||
public String getUserId();
|
public String getUserId();
|
||||||
|
|
||||||
|
void setLoginUsername(String loginUsername);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
// 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) {
|
if (restart.get() || "ha".equals(cacheMode) || shouldSetUpDb.get() || configuration.getFipsMode() != FipsMode.DISABLED) {
|
||||||
commands.removeIf("--optimized"::equals);
|
prepareCommandsForRebuilding(commands);
|
||||||
commands.add("--http-relative-path=/auth");
|
|
||||||
|
|
||||||
if ("local".equals(cacheMode)) {
|
if ("local".equals(cacheMode)) {
|
||||||
commands.add("--cache=local");
|
commands.add("--cache=local");
|
||||||
|
@ -213,6 +212,15 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
|
||||||
return commands;
|
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) {
|
protected void addFeaturesOption(List<String> commands) {
|
||||||
String defaultFeatures = configuration.getDefaultFeatures();
|
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());
|
commands.add(featuresOption.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -304,6 +304,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||||
String userSessionId = KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> {
|
String userSessionId = KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> {
|
||||||
RealmModel realm = kcSession.realms().getRealmByName("test");
|
RealmModel realm = kcSession.realms().getRealmByName("test");
|
||||||
UserSessionModel userSession = createSessions(kcSession)[0];
|
UserSessionModel userSession = createSessions(kcSession)[0];
|
||||||
|
userSession = kcSession.sessions().getUserSession(realm, userSession.getId());
|
||||||
|
|
||||||
kcSession.sessions().removeUserSession(realm, userSession);
|
kcSession.sessions().removeUserSession(realm, userSession);
|
||||||
return userSession.getId();
|
return userSession.getId();
|
||||||
|
@ -559,9 +560,11 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||||
public void testRemovingExpiredSession(KeycloakSession session) {
|
public void testRemovingExpiredSession(KeycloakSession session) {
|
||||||
UserSessionModel[] sessions = createSessions(session);
|
UserSessionModel[] sessions = createSessions(session);
|
||||||
try {
|
try {
|
||||||
Time.setOffset(3600000);
|
|
||||||
UserSessionModel userSession = sessions[0];
|
UserSessionModel userSession = sessions[0];
|
||||||
RealmModel realm = userSession.getRealm();
|
RealmModel realm = userSession.getRealm();
|
||||||
|
// reload userSession in current session
|
||||||
|
userSession = session.sessions().getUserSession(realm, userSession.getId());
|
||||||
|
Time.setOffset(3600000);
|
||||||
session.sessions().removeExpired(realm);
|
session.sessions().removeExpired(realm);
|
||||||
|
|
||||||
// Assert no exception is thrown here
|
// Assert no exception is thrown here
|
||||||
|
|
|
@ -522,6 +522,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
// Refresh with the offline token
|
// Refresh with the offline token
|
||||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||||
|
Assert.assertNull("received error " + tokenResponse.getError() + ", " + tokenResponse.getErrorDescription(), tokenResponse.getError());
|
||||||
|
|
||||||
// Use accessToken to admin REST request
|
// Use accessToken to admin REST request
|
||||||
try (Keycloak offlineTokenAdmin = Keycloak.getInstance(getAuthServerContextRoot() + "/auth",
|
try (Keycloak offlineTokenAdmin = Keycloak.getInstance(getAuthServerContextRoot() + "/auth",
|
||||||
|
|
|
@ -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
|
|
@ -212,6 +212,13 @@
|
||||||
</properties>
|
</properties>
|
||||||
</profile>
|
</profile>
|
||||||
|
|
||||||
|
<profile>
|
||||||
|
<id>jpa+infinispan+persistentsessions</id>
|
||||||
|
<properties>
|
||||||
|
<keycloak.model.parameters>Infinispan,Jpa,PersistentUserSessions</keycloak.model.parameters>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
|
||||||
<profile>
|
<profile>
|
||||||
<id>jpa+infinispan+client-storage</id>
|
<id>jpa+infinispan+client-storage</id>
|
||||||
<properties>
|
<properties>
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.UserSessionProvider;
|
import org.keycloak.models.UserSessionProvider;
|
||||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||||
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
|
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
|
||||||
|
import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||||
import org.keycloak.testsuite.model.RequireProvider;
|
import org.keycloak.testsuite.model.RequireProvider;
|
||||||
|
@ -258,7 +259,13 @@ public class OfflineSessionPersistenceTest extends KeycloakModelTest {
|
||||||
// Delete local user cache (persisted sessions are still kept)
|
// Delete local user cache (persisted sessions are still kept)
|
||||||
UserSessionProvider provider = session.getProvider(UserSessionProvider.class);
|
UserSessionProvider provider = session.getProvider(UserSessionProvider.class);
|
||||||
// Remove in-memory representation of the offline sessions
|
// Remove in-memory representation of the offline sessions
|
||||||
|
if (provider instanceof InfinispanUserSessionProvider) {
|
||||||
((InfinispanUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true);
|
((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;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.model.session;
|
package org.keycloak.testsuite.model.session;
|
||||||
|
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.client.hotrod.RemoteCache;
|
import org.infinispan.client.hotrod.RemoteCache;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
import org.junit.Assume;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
@ -155,6 +158,9 @@ public class UserSessionInitializerTest extends KeycloakModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUserSessionPropagationBetweenSites() throws InterruptedException {
|
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();
|
AtomicInteger index = new AtomicInteger();
|
||||||
AtomicReference<String> userSessionId = new AtomicReference<>();
|
AtomicReference<String> userSessionId = new AtomicReference<>();
|
||||||
AtomicReference<List<Boolean>> containsSession = new AtomicReference<>(new LinkedList<>());
|
AtomicReference<List<Boolean>> containsSession = new AtomicReference<>(new LinkedList<>());
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.model.session;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -144,6 +145,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||||
.forEach(userSessionLooper -> persistUserSession(session, userSessionLooper, true));
|
.forEach(userSessionLooper -> persistUserSession(session, userSessionLooper, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) {
|
||||||
inComittedTransaction(session -> {
|
inComittedTransaction(session -> {
|
||||||
// Persist 1 online session
|
// Persist 1 online session
|
||||||
RealmModel realm = session.realms().getRealm(realmId);
|
RealmModel realm = session.realms().getRealm(realmId);
|
||||||
|
@ -156,6 +158,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||||
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, false, 1, 1, 1);
|
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");
|
assertSession(loadedSessions.get(0), session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
inComittedTransaction(session -> {
|
inComittedTransaction(session -> {
|
||||||
// Assert offline sessions
|
// 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);
|
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());
|
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 -> {
|
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);
|
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());
|
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");
|
||||||
createClientSession(session, realmId, fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state");
|
createClientSession(session, fooRealm.getId(), fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state");
|
||||||
});
|
});
|
||||||
|
|
||||||
inComittedTransaction(session -> {
|
inComittedTransaction(session -> {
|
||||||
|
|
|
@ -101,8 +101,8 @@ public class UserSessionProviderModelTest extends KeycloakModelTest {
|
||||||
inComittedTransaction(session -> {
|
inComittedTransaction(session -> {
|
||||||
RealmModel realm = session.realms().getRealm(realmId);
|
RealmModel realm = session.realms().getRealm(realmId);
|
||||||
|
|
||||||
session.sessions().removeUserSession(realm, origSessions[0]);
|
session.sessions().removeUserSession(realm, session.sessions().getUserSession(realm, origSessions[0].getId()));
|
||||||
session.sessions().removeUserSession(realm, origSessions[1]);
|
session.sessions().removeUserSession(realm, session.sessions().getUserSession(realm, origSessions[1].getId()));
|
||||||
});
|
});
|
||||||
|
|
||||||
inComittedTransaction(session -> {
|
inComittedTransaction(session -> {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.infinispan.context.Flag;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
@ -300,10 +301,20 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
|
||||||
session.sessions().createOfflineUserSession(userSession);
|
session.sessions().createOfflineUserSession(userSession);
|
||||||
session.sessions().createOfflineUserSession(origSessions[0]);
|
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
|
// try to load user session from persister
|
||||||
Assert.assertEquals(2, persister.loadUserSessionsStream(0, 10, true, "00000000-0000-0000-0000-000000000000").count());
|
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 {
|
} finally {
|
||||||
setTimeOffset(0);
|
setTimeOffset(0);
|
||||||
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
|
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
|
||||||
|
|
Loading…
Reference in a new issue