From d6422e415c02e6831a9268b396ea2f8cf554f3f1 Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Tue, 1 Dec 2020 15:11:31 -0300 Subject: [PATCH] [KEYCLOAK-16508] Complement methods for accessing user sessions with Stream variants --- .../InfinispanUserSessionProvider.java | 99 ++++--------- .../keycloak/models/UserSessionProvider.java | 139 +++++++++++++++++- .../requiredactions/UpdatePassword.java | 13 +- .../keycloak/broker/saml/SAMLEndpoint.java | 53 ++++--- .../oidc/endpoints/LogoutEndpoint.java | 29 ++-- .../endpoints/TokenRevocationEndpoint.java | 17 +-- .../managers/AuthenticationManager.java | 18 +-- .../managers/ResourceAdminManager.java | 33 ----- .../services/managers/UserSessionManager.java | 56 +++---- .../resources/account/AccountFormService.java | 22 +-- .../resources/account/AccountRestService.java | 53 +++---- .../resources/account/SessionResource.java | 57 ++++--- .../resources/admin/ClientResource.java | 56 ++++--- .../resources/admin/UserResource.java | 59 ++++---- .../testsuite/admin/ImpersonationTest.java | 2 +- .../broker/BrokerRunOnServerUtil.java | 3 +- .../model/UserSessionInitializerTest.java | 7 +- .../UserSessionPersisterProviderTest.java | 7 +- .../model/UserSessionProviderOfflineTest.java | 19 ++- .../model/UserSessionProviderTest.java | 42 +++--- 20 files changed, 406 insertions(+), 378 deletions(-) diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index b5a7eb75a7..9b988f2b56 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -67,8 +67,6 @@ import java.io.Serializable; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -80,6 +78,7 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** * @author Stian Thorgersen @@ -270,25 +269,16 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } - protected List getUserSessions(RealmModel realm, Predicate>> predicate, boolean offline) { + protected Stream getUserSessionsStream(RealmModel realm, Predicate>> predicate, boolean offline) { Cache> cache = getCache(offline); - cache = CacheDecorators.skipCacheLoaders(cache); - Stream>> cacheStream = cache.entrySet().stream(); - - List resultSessions = new LinkedList<>(); - - Iterator itr = cacheStream.filter(predicate) + // return a stream that 'wraps' the infinispan cache stream so that the cache stream's elements are read one by one + // and then filtered/mapped locally to avoid serialization issues when trying to manipulate the cache stream directly. + return StreamSupport.stream(cache.entrySet().stream().spliterator(), true) + .filter(predicate) .map(Mappers.userSessionEntity()) - .iterator(); - - while (itr.hasNext()) { - UserSessionEntity userSessionEntity = itr.next(); - resultSessions.add(wrap(realm, userSessionEntity, offline)); - } - - return resultSessions; + .map(entity -> this.wrap(realm, entity, offline)); } @Override @@ -305,46 +295,45 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override - public List getUserSessions(final RealmModel realm, UserModel user) { - return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), false); + public Stream getUserSessionsStream(final RealmModel realm, UserModel user) { + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), false); } @Override - public List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId) { - return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), false); + public Stream getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) { + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), false); } @Override public UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { - List userSessions = getUserSessions(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), false); - return userSessions.isEmpty() ? null : userSessions.get(0); + return this.getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), false) + .findFirst().orElse(null); } @Override - public List getUserSessions(RealmModel realm, ClientModel client) { - return getUserSessions(realm, client, -1, -1); + public Stream getUserSessionsStream(RealmModel realm, ClientModel client) { + return getUserSessionsStream(realm, client, -1, -1); } @Override - public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { - return getUserSessions(realm, client, firstResult, maxResults, false); + public Stream getUserSessionsStream(RealmModel realm, ClientModel client, int firstResult, int maxResults) { + return getUserSessionsStream(realm, client, firstResult, maxResults, false); } - protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { + protected Stream getUserSessionsStream(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { final String clientUuid = client.getId(); UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(clientUuid); return getUserSessionModels(realm, firstResult, maxResults, offline, predicate); } - protected List getUserSessionModels(RealmModel realm, int firstResult, int maxResults, boolean offline, UserSessionPredicate predicate) { + protected Stream getUserSessionModels(RealmModel realm, int firstResult, int maxResults, boolean offline, UserSessionPredicate predicate) { Cache> cache = getCache(offline); cache = CacheDecorators.skipCacheLoaders(cache); - Cache> clientSessionCache = getClientSessionCache(offline); - Cache> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache); - - Stream stream = cache.entrySet().stream() + // return a stream that 'wraps' the infinispan cache stream so that the cache stream's elements are read one by one + // and then filtered/mapped locally to avoid serialization issues when trying to manipulate the cache stream directly. + Stream stream = StreamSupport.stream(cache.entrySet().stream().spliterator(), true) .filter(predicate) .map(Mappers.userSessionEntity()) .sorted(Comparators.userSessionLastSessionRefresh()); @@ -357,16 +346,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { stream = stream.limit(maxResults); } - final List sessions = new LinkedList<>(); - Iterator itr = stream.iterator(); - - while (itr.hasNext()) { - UserSessionEntity userSessionEntity = itr.next(); - sessions.add(wrap(realm, userSessionEntity, offline)); - } - - - return sessions; + return stream.map(entity -> this.wrap(realm, entity, offline)); } @Override @@ -839,13 +819,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public UserSessionModel getOfflineUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { - List userSessions = getUserSessions(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), true); - return userSessions.isEmpty() ? null : userSessions.get(0); + return this.getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), true) + .findFirst().orElse(null); } @Override - public List getOfflineUserSessionByBrokerUserId(RealmModel realm, String brokerUserId) { - return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), true); + public Stream getOfflineUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) { + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), true); } @Override @@ -856,8 +836,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } } - - @Override public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) { UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : @@ -874,23 +852,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public List getOfflineUserSessions(RealmModel realm, UserModel user) { - List userSessions = new LinkedList<>(); - - Cache> cache = CacheDecorators.skipCacheLoaders(offlineSessionCache); - - Iterator itr = cache.entrySet().stream() - .filter(UserSessionPredicate.create(realm.getId()).user(user.getId())) - .map(Mappers.userSessionEntity()) - .iterator(); - - while (itr.hasNext()) { - UserSessionEntity userSessionEntity = itr.next(); - UserSessionModel userSession = wrap(realm, userSessionEntity, true); - userSessions.add(userSession); - } - - return userSessions; + public Stream getOfflineUserSessionsStream(RealmModel realm, UserModel user) { + return this.getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), true); } @Override @@ -899,8 +862,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) { - return getUserSessions(realm, client, first, max, true); + public Stream getOfflineUserSessionsStream(RealmModel realm, ClientModel client, int first, int max) { + return getUserSessionsStream(realm, client, first, max, true); } diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index 538ef76922..0db20f5759 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -24,6 +24,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Bill Burke @@ -40,10 +42,79 @@ public interface UserSessionProvider extends Provider { String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState); UserSessionModel getUserSession(RealmModel realm, String id); - List getUserSessions(RealmModel realm, UserModel user); - List getUserSessions(RealmModel realm, ClientModel client); - List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults); - List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId); + + /** + * @deprecated Use {@link #getUserSessionsStream(RealmModel, ClientModel) getUserSessionsStream} instead. + */ + @Deprecated + default List getUserSessions(RealmModel realm, UserModel user) { + return this.getUserSessionsStream(realm, user).collect(Collectors.toList()); + } + + /** + * Obtains the user sessions associated with the specified user. + * + * @param realm a reference to the realm. + * @param user the user whose sessions are being searched. + * @return a non-null {@link Stream} of user sessions. + */ + Stream getUserSessionsStream(RealmModel realm, UserModel user); + + /** + * @deprecated Use {@link #getUserSessionsStream(RealmModel, ClientModel) getUserSessionsStream} instead. + */ + @Deprecated + default List getUserSessions(RealmModel realm, ClientModel client) { + return this.getUserSessionsStream(realm, client).collect(Collectors.toList()); + } + + /** + * Obtains the user sessions associated with the specified client. + * + * @param realm a reference to the realm. + * @param client the client whose user sessions are being searched. + * @return a non-null {@link Stream} of user sessions. + */ + Stream getUserSessionsStream(RealmModel realm, ClientModel client); + + /** + * @deprecated Use {@link #getUserSessionsStream(RealmModel, ClientModel, int, int) getUserSessionsStream} instead. + */ + @Deprecated + default List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { + return this.getUserSessionsStream(realm, client, firstResult, maxResults).collect(Collectors.toList()); + } + + /** + * Obtains the user sessions associated with the specified client, starting from the {@code firstResult} and containing + * at most {@code maxResults}. + * + * @param realm a reference tot he realm. + * @param client the client whose user sessions are being searched. + * @param firstResult first result to return. Ignored if negative. + * @param maxResults maximum number of results to return. Ignored if negative. + * @return a non-null {@link Stream} of user sessions. + */ + Stream getUserSessionsStream(RealmModel realm, ClientModel client, int firstResult, int maxResults); + + /** + * @deprecated Use {@link #getUserSessionByBrokerUserIdStream(RealmModel, String) getUserSessionByBrokerUserIdStream} + * instead. + */ + @Deprecated + default List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId) { + return this.getUserSessionByBrokerUserIdStream(realm, brokerUserId).collect(Collectors.toList()); + } + + /** + * Obtains the user sessions associated with the user that matches the specified {@code brokerUserId}. + * + * @param realm a reference to the realm. + * @param brokerUserId the id of the broker user whose sessions are being searched. + * @return a non-null {@link Stream} of user sessions. + */ + Stream getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId); + UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId); /** @@ -88,12 +159,66 @@ public interface UserSessionProvider extends Provider { /** Will automatically attach newly created offline client session to the offlineUserSession **/ AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession); - List getOfflineUserSessions(RealmModel realm, UserModel user); + + /** + * @deprecated Use {@link #getOfflineUserSessionsStream(RealmModel, UserModel) getOfflineUserSessionsStream} instead. + */ + @Deprecated + default List getOfflineUserSessions(RealmModel realm, UserModel user) { + return this.getOfflineUserSessionsStream(realm, user).collect(Collectors.toList()); + } + + /** + * Obtains the offline user sessions associated with the specified user. + * + * @param realm a reference to the realm. + * @param user the user whose offline sessions are being searched. + * @return a non-null {@link Stream} of offline user sessions. + */ + Stream getOfflineUserSessionsStream(RealmModel realm, UserModel user); + UserSessionModel getOfflineUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId); - List getOfflineUserSessionByBrokerUserId(RealmModel realm, String brokerUserId); + + /** + * @deprecated Use {@link #getOfflineUserSessionByBrokerUserIdStream(RealmModel, String) getOfflineUserSessionByBrokerUserIdStream} + * instead. + */ + @Deprecated + default List getOfflineUserSessionByBrokerUserId(RealmModel realm, String brokerUserId) { + return this.getOfflineUserSessionByBrokerUserIdStream(realm, brokerUserId).collect(Collectors.toList()); + } + + /** + * Obtains the offline user sessions associated with the user that matches the specified {@code brokerUserId}. + * + * @param realm a reference to the realm. + * @param brokerUserId the id of the broker user whose sessions are being searched. + * @return a non-null {@link Stream} of offline user sessions. + */ + Stream getOfflineUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId); long getOfflineSessionsCount(RealmModel realm, ClientModel client); - List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max); + + /** + * @deprecated use {@link #getOfflineUserSessionsStream(RealmModel, ClientModel, int, int) getOfflineUserSessionsStream} + * instead. + */ + @Deprecated + default List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) { + return this.getOfflineUserSessionsStream(realm, client, first, max).collect(Collectors.toList()); + } + + /** + * Obtains the offline user sessions associated with the specified client, starting from the {@code firstResult} and + * containing at most {@code maxResults}. + * + * @param realm a reference tot he realm. + * @param client the client whose user sessions are being searched. + * @param firstResult first result to return. Ignored if negative. + * @param maxResults maximum number of results to return. Ignored if negative. + * @return a non-null {@link Stream} of offline user sessions. + */ + Stream getOfflineUserSessionsStream(RealmModel realm, ClientModel client, int firstResult, int maxResults); /** Triggered by persister during pre-load. It imports authenticatedClientSessions too **/ void importUserSessions(Collection persistentUserSessions, boolean offline); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java index b8133397bb..77ee2ed0d6 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java @@ -47,7 +47,9 @@ import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * @author Bill Burke @@ -129,12 +131,11 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac if (getId().equals(authSession.getClientNote(Constants.KC_ACTION_EXECUTING)) && "on".equals(formData.getFirst("logout-sessions"))) { - List sessions = session.sessions().getUserSessions(realm, user); - for (UserSessionModel s : sessions) { - if (!s.getId().equals(authSession.getParentSession().getId())) { - AuthenticationManager.backchannelLogout(session, realm, s, session.getContext().getUri(), context.getConnection(), context.getHttpRequest().getHttpHeaders(), true); - } - } + session.sessions().getUserSessionsStream(realm, user) + .filter(s -> !Objects.equals(s.getId(), authSession.getParentSession().getId())) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(s -> AuthenticationManager.backchannelLogout(session, realm, s, session.getContext().getUri(), + context.getConnection(), context.getHttpRequest().getHttpHeaders(), true)); } try { diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java index adf48c5aa8..4c65e74e57 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -83,9 +83,15 @@ import javax.xml.namespace.QName; import java.io.IOException; import java.security.Key; import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.keycloak.protocol.saml.SamlPrincipalType; import org.keycloak.rotation.HardcodedKeyLocator; @@ -93,14 +99,14 @@ import org.keycloak.rotation.KeyLocator; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.saml.validators.ConditionsValidator; import org.keycloak.saml.validators.DestinationValidator; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + import java.net.URI; import java.security.cert.CertificateException; -import org.w3c.dom.Element; -import java.util.*; import javax.ws.rs.core.MultivaluedMap; import javax.xml.crypto.dsig.XMLSignature; -import org.w3c.dom.NodeList; /** * @author Bill Burke @@ -296,22 +302,13 @@ public class SAMLEndpoint { protected Response logoutRequest(LogoutRequestType request, String relayState) { String brokerUserId = config.getAlias() + "." + request.getNameID().getValue(); if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) { - List userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId); - for (UserSessionModel userSession : userSessions) { - if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) { - continue; - } - - for(Iterator it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) { - request = it.next().beforeProcessingLogoutRequest(request, userSession, null); - } - - try { - AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, false); - } catch (Exception e) { - logger.warn("failed to do backchannel logout for userSession", e); - } - } + AtomicReference ref = new AtomicReference<>(request); + session.sessions().getUserSessionByBrokerUserIdStream(realm, brokerUserId) + .filter(userSession -> userSession.getState() != UserSessionModel.State.LOGGING_OUT && + userSession.getState() != UserSessionModel.State.LOGGED_OUT) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(processLogout(ref)); + request = ref.get(); } else { for (String sessionIndex : request.getSessionIndex()) { @@ -369,6 +366,19 @@ public class SAMLEndpoint { } + private Consumer processLogout(AtomicReference ref) { + return userSession -> { + for(Iterator it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) { + ref.set(it.next().beforeProcessingLogoutRequest(ref.get(), userSession, null)); + } + try { + AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, false); + } catch (Exception e) { + logger.warn("failed to do backchannel logout for userSession", e); + } + }; + } + private String getEntityId(UriInfo uriInfo, RealmModel realm) { String configEntityId = config.getEntityId(); @@ -578,11 +588,6 @@ public class SAMLEndpoint { } return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers); } - - - - - } protected class PostBinding extends Binding { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 1265496db3..c32dd13a7f 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -65,8 +65,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; -import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.keycloak.models.UserSessionModel.State.LOGGED_OUT; @@ -356,34 +356,29 @@ public class LogoutEndpoint { BackchannelLogoutResponse backchannelLogoutResponse = new BackchannelLogoutResponse(); backchannelLogoutResponse.setLocalLogoutSucceeded(true); identityProviderAliases.forEach(identityProviderAlias -> { - List userSessions = session.sessions().getUserSessionByBrokerUserId(realm, - identityProviderAlias + "." + federatedUserId); if (logoutOfflineSessions) { logoutOfflineUserSessions(identityProviderAlias + "." + federatedUserId); } - for (UserSessionModel userSession : userSessions) { - BackchannelLogoutResponse userBackchannelLogoutResponse; - userBackchannelLogoutResponse = logoutUserSession(userSession); - backchannelLogoutResponse.setLocalLogoutSucceeded(backchannelLogoutResponse.getLocalLogoutSucceeded() - && userBackchannelLogoutResponse.getLocalLogoutSucceeded()); - userBackchannelLogoutResponse.getClientResponses() - .forEach(backchannelLogoutResponse::addClientResponses); - } + session.sessions().getUserSessionByBrokerUserIdStream(realm, identityProviderAlias + "." + federatedUserId) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(userSession -> { + BackchannelLogoutResponse userBackchannelLogoutResponse = this.logoutUserSession(userSession); + backchannelLogoutResponse.setLocalLogoutSucceeded(backchannelLogoutResponse.getLocalLogoutSucceeded() + && userBackchannelLogoutResponse.getLocalLogoutSucceeded()); + userBackchannelLogoutResponse.getClientResponses() + .forEach(backchannelLogoutResponse::addClientResponses); + }); }); return backchannelLogoutResponse; } private void logoutOfflineUserSessions(String brokerUserId) { - List offlineUserSessions = - session.sessions().getOfflineUserSessionByBrokerUserId(realm, brokerUserId); - UserSessionManager userSessionManager = new UserSessionManager(session); - for (UserSessionModel offlineUserSession : offlineUserSessions) { - userSessionManager.revokeOfflineUserSession(offlineUserSession); - } + session.sessions().getOfflineUserSessionByBrokerUserIdStream(realm, brokerUserId).collect(Collectors.toList()) + .forEach(userSessionManager::revokeOfflineUserSession); } private BackchannelLogoutResponse logoutUserSession(UserSessionModel userSession) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index b0c7754103..33c9818203 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -17,7 +17,8 @@ package org.keycloak.protocol.oidc.endpoints; -import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import javax.ws.rs.Consumes; import javax.ws.rs.POST; @@ -36,7 +37,6 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.headers.SecurityHeadersProvider; -import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -223,14 +223,11 @@ public class TokenRevocationEndpoint { if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType())) { new UserSessionManager(session).revokeOfflineToken(user, client); } - - List userSessions = session.sessions().getUserSessions(realm, user); - for (UserSessionModel userSession : userSessions) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (clientSession != null) { - org.keycloak.protocol.oidc.TokenManager.dettachClientSession(session.sessions(), realm, clientSession); - } - } + session.sessions().getUserSessionsStream(realm, user) + .map(userSession -> userSession.getAuthenticatedClientSessionByClient(client.getId())) + .filter(Objects::nonNull) + .collect(Collectors.toList()) // collect to avoid concurrent modification as dettachClientSession removes the user sessions. + .forEach(clientSession -> TokenManager.dettachClientSession(session.sessions(), realm, clientSession)); } private void revokeAccessToken() { diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index f3646f0c0c..1b83ef23b7 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -548,15 +548,15 @@ public class AuthenticationManager { * @param headers */ public static void backchannelLogoutUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) { - List userSessions = session.sessions().getUserSessions(realm, user); - for (UserSessionModel userSession : userSessions) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (clientSession != null) { - backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers); - clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name()); - org.keycloak.protocol.oidc.TokenManager.dettachClientSession(session.sessions(), realm, clientSession); - } - } + session.sessions().getUserSessionsStream(realm, user) + .map(userSession -> userSession.getAuthenticatedClientSessionByClient(client.getId())) + .filter(Objects::nonNull) + .collect(Collectors.toList()) // collect to avoid concurrent modification. + .forEach(clientSession -> { + backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers); + clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name()); + TokenManager.dettachClientSession(session.sessions(), realm, clientSession); + }); } public static Response browserLogout(KeycloakSession session, diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index c72cd1f8e1..d16f6a7cdb 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -37,7 +37,6 @@ 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.protocol.LoginProtocol; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -119,38 +118,6 @@ public class ResourceAdminManager { return result; } - public void logoutUser(RealmModel realm, UserModel user, KeycloakSession keycloakSession) { - keycloakSession.users().setNotBeforeForUser(realm, user, Time.currentTime()); - - List userSessions = keycloakSession.sessions().getUserSessions(realm, user); - logoutUserSessions(realm, userSessions); - } - - protected void logoutUserSessions(RealmModel realm, List userSessions) { - // Map from "app" to clientSessions for this app - MultivaluedHashMap clientSessions = new MultivaluedHashMap<>(); - for (UserSessionModel userSession : userSessions) { - putClientSessions(clientSessions, userSession); - } - - logger.debugv("logging out {0} resources ", clientSessions.size()); - //logger.infov("logging out resources: {0}", clientSessions); - - for (Map.Entry> entry : clientSessions.entrySet()) { - if (entry.getValue().size() == 0) { - continue; - } - logoutClientSessions(realm, entry.getValue().get(0).getClient(), entry.getValue()); - } - } - - private void putClientSessions(MultivaluedHashMap clientSessions, UserSessionModel userSession) { - for (Map.Entry entry : userSession.getAuthenticatedClientSessions().entrySet()) { - clientSessions.add(entry.getKey(), entry.getValue()); - } - } - - public Response logoutClientSession(RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) { return logoutClientSessions(realm, resource, Arrays.asList(clientSession)); } diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java index 653b9c29ac..c8a90be521 100644 --- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java @@ -30,10 +30,11 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.services.ServicesLogger; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import java.util.stream.Stream; /** @@ -77,43 +78,42 @@ public class UserSessionManager { } public Set findClientsWithOfflineToken(RealmModel realm, UserModel user) { - List userSessions = kcSession.sessions().getOfflineUserSessions(realm, user); - Set clients = new HashSet<>(); - for (UserSessionModel userSession : userSessions) { - Set clientIds = userSession.getAuthenticatedClientSessions().keySet(); - for (String clientUUID : clientIds) { - ClientModel client = realm.getClientById(clientUUID); - clients.add(client); - } - } - return clients; + return kcSession.sessions().getOfflineUserSessionsStream(realm, user) + .flatMap(userSession -> userSession.getAuthenticatedClientSessions().keySet().stream()) + .map(clientUUID -> realm.getClientById(clientUUID)) + .collect(Collectors.toSet()); } + @Deprecated public List findOfflineSessions(RealmModel realm, UserModel user) { - return kcSession.sessions().getOfflineUserSessions(realm, user); + return this.findOfflineSessionsStream(realm, user).collect(Collectors.toList()); + } + + public Stream findOfflineSessionsStream(RealmModel realm, UserModel user) { + return kcSession.sessions().getOfflineUserSessionsStream(realm, user); } public boolean revokeOfflineToken(UserModel user, ClientModel client) { RealmModel realm = client.getRealm(); - List userSessions = kcSession.sessions().getOfflineUserSessions(realm, user); - boolean anyRemoved = false; - for (UserSessionModel userSession : userSessions) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (clientSession != null) { - if (logger.isTraceEnabled()) { - logger.tracef("Removing existing offline token for user '%s' and client '%s' .", - user.getUsername(), client.getClientId()); - } + AtomicBoolean anyRemoved = new AtomicBoolean(false); + kcSession.sessions().getOfflineUserSessionsStream(realm, user).collect(Collectors.toList()) + .forEach(userSession -> { + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); + if (clientSession != null) { + if (logger.isTraceEnabled()) { + logger.tracef("Removing existing offline token for user '%s' and client '%s' .", + user.getUsername(), client.getClientId()); + } - clientSession.detachFromUserSession(); - persister.removeClientSession(userSession.getId(), client.getId(), true); - checkOfflineUserSessionHasClientSessions(realm, user, userSession); - anyRemoved = true; - } - } + clientSession.detachFromUserSession(); + persister.removeClientSession(userSession.getId(), client.getId(), true); + checkOfflineUserSessionHasClientSessions(realm, user, userSession); + anyRemoved.set(true); + } + }); - return anyRemoved; + return anyRemoved.get(); } public void revokeOfflineUserSession(UserSessionModel userSession) { diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index ec758021b4..f943a7e793 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -322,7 +322,7 @@ public class AccountFormService extends AbstractSecuredLocalService { @GET public Response sessionsPage() { if (auth != null) { - account.setSessions(session.sessions().getUserSessions(realm, auth.getUser())); + account.setSessions(session.sessions().getUserSessionsStream(realm, auth.getUser()).collect(Collectors.toList())); } return forwardToPage("sessions", AccountPages.SESSIONS); } @@ -342,7 +342,6 @@ public class AccountFormService extends AbstractSecuredLocalService { * lastName * email * - * @param formData * @return */ @Path("/") @@ -427,10 +426,10 @@ public class AccountFormService extends AbstractSecuredLocalService { // as time on the token will be same like notBefore session.users().setNotBeforeForUser(realm, user, Time.currentTime() - 1); - List userSessions = session.sessions().getUserSessions(realm, user); - for (UserSessionModel userSession : userSessions) { - AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true); - } + session.sessions().getUserSessionsStream(realm, user) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(userSession -> AuthenticationManager.backchannelLogout(session, realm, userSession, + session.getContext().getUri(), clientConnection, headers, true)); UriBuilder builder = Urls.accountBase(session.getContext().getUri().getBaseUri()).path(AccountFormService.class, "sessionsPage"); String referrer = session.getContext().getUri().getQueryParameters().getFirst("referrer"); @@ -495,7 +494,6 @@ public class AccountFormService extends AbstractSecuredLocalService { * totp - otp generated by authenticator * totpSecret - totp secret to register * - * @param formData * @return */ @Path("totp") @@ -567,7 +565,6 @@ public class AccountFormService extends AbstractSecuredLocalService { * password-new * pasword-confirm * - * @param formData * @return */ @Path("password") @@ -641,12 +638,9 @@ public class AccountFormService extends AbstractSecuredLocalService { return account.setError(Response.Status.INTERNAL_SERVER_ERROR, ape.getMessage()).createResponse(AccountPages.PASSWORD); } - List sessions = session.sessions().getUserSessions(realm, user); - for (UserSessionModel s : sessions) { - if (!s.getId().equals(auth.getSession().getId())) { - AuthenticationManager.backchannelLogout(session, realm, s, session.getContext().getUri(), clientConnection, headers, true); - } - } + session.sessions().getUserSessionsStream(realm, user).filter(s -> !Objects.equals(s.getId(), auth.getSession().getId())) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(s -> AuthenticationManager.backchannelLogout(session, realm, s, session.getContext().getUri(), clientConnection, headers, true)); event.event(EventType.UPDATE_PASSWORD).client(auth.getClient()).user(auth.getUser()).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index 9f1bc330da..a2077c0c59 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -33,7 +33,6 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; import org.keycloak.representations.account.ClientRepresentation; import org.keycloak.representations.account.ConsentRepresentation; import org.keycloak.representations.account.ConsentScopeRepresentation; @@ -79,6 +78,7 @@ import java.util.Properties; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Stian Thorgersen @@ -401,51 +401,36 @@ public class AccountRestService { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public List applications(@QueryParam("name") String name) { + public Stream applications(@QueryParam("name") String name) { checkAccountApiEnabled(); auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_APPLICATIONS); Set clients = new HashSet<>(); List inUseClients = new LinkedList<>(); - List sessions = session.sessions().getUserSessions(realm, user); - for(UserSessionModel s : sessions) { - for (AuthenticatedClientSessionModel a : s.getAuthenticatedClientSessions().values()) { - ClientModel client = a.getClient(); - clients.add(client); - inUseClients.add(client.getClientId()); - } - } + clients.addAll(session.sessions().getUserSessionsStream(realm, user) + .flatMap(s -> s.getAuthenticatedClientSessions().values().stream()) + .map(AuthenticatedClientSessionModel::getClient) + .peek(client -> inUseClients.add(client.getClientId())) + .collect(Collectors.toSet())); List offlineClients = new LinkedList<>(); - List offlineSessions = session.sessions().getOfflineUserSessions(realm, user); - for(UserSessionModel s : offlineSessions) { - for(AuthenticatedClientSessionModel a : s.getAuthenticatedClientSessions().values()) { - ClientModel client = a.getClient(); - clients.add(client); - offlineClients.add(client.getClientId()); - } - } + clients.addAll(session.sessions().getOfflineUserSessionsStream(realm, user) + .flatMap(s -> s.getAuthenticatedClientSessions().values().stream()) + .map(AuthenticatedClientSessionModel::getClient) + .peek(client -> offlineClients.add(client.getClientId())) + .collect(Collectors.toSet())); Map consentModels = new HashMap<>(); - session.users().getConsentsStream(realm, user.getId()).forEach(consent -> { - ClientModel client = consent.getClient(); - clients.add(client); - consentModels.put(client.getClientId(), consent); - }); + clients.addAll(session.users().getConsentsStream(realm, user.getId()) + .peek(consent -> consentModels.put(consent.getClient().getClientId(), consent)) + .map(UserConsentModel::getClient) + .collect(Collectors.toSet())); realm.getAlwaysDisplayInConsoleClientsStream().forEach(clients::add); - List apps = new LinkedList<>(); - for (ClientModel client : clients) { - if (client.isBearerOnly() || client.getBaseUrl() == null || client.getBaseUrl().isEmpty()) { - continue; - } - else if (matches(client, name)) { - apps.add(modelToRepresentation(client, inUseClients, offlineClients, consentModels)); - } - } - - return apps; + return clients.stream().filter(client -> !client.isBearerOnly() && client.getBaseUrl() != null && !client.getClientId().isEmpty()) + .filter(client -> matches(client, name)) + .map(client -> modelToRepresentation(client, inUseClients, offlineClients, consentModels)); } private boolean matches(ClientModel client, String name) { diff --git a/services/src/main/java/org/keycloak/services/resources/account/SessionResource.java b/services/src/main/java/org/keycloak/services/resources/account/SessionResource.java index 46990a9d5b..5d816118c3 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/SessionResource.java +++ b/services/src/main/java/org/keycloak/services/resources/account/SessionResource.java @@ -30,6 +30,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; @@ -73,8 +74,8 @@ public class SessionResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public List toRepresentation() { - return session.sessions().getUserSessions(realm, user).stream().map(this::toRepresentation).collect(Collectors.toList()); + public Stream toRepresentation() { + return session.sessions().getUserSessionsStream(realm, user).map(this::toRepresentation); } /** @@ -88,33 +89,31 @@ public class SessionResource { @NoCache public Collection devices() { Map reps = new HashMap<>(); - List sessions = session.sessions().getUserSessions(realm, user); + session.sessions().getUserSessionsStream(realm, user).forEach(s -> { + DeviceRepresentation device = getAttachedDevice(s); + DeviceRepresentation rep = reps + .computeIfAbsent(device.getOs() + device.getOsVersion(), key -> { + DeviceRepresentation representation = new DeviceRepresentation(); - for (UserSessionModel s : sessions) { - DeviceRepresentation device = getAttachedDevice(s); - DeviceRepresentation rep = reps - .computeIfAbsent(device.getOs() + device.getOsVersion(), key -> { - DeviceRepresentation representation = new DeviceRepresentation(); + representation.setLastAccess(device.getLastAccess()); + representation.setOs(device.getOs()); + representation.setOsVersion(device.getOsVersion()); + representation.setDevice(device.getDevice()); + representation.setMobile(device.isMobile()); - representation.setLastAccess(device.getLastAccess()); - representation.setOs(device.getOs()); - representation.setOsVersion(device.getOsVersion()); - representation.setDevice(device.getDevice()); - representation.setMobile(device.isMobile()); + return representation; + }); - return representation; - }); + if (isCurrentSession(s)) { + rep.setCurrent(true); + } - if (isCurrentSession(s)) { - rep.setCurrent(true); - } + if (rep.getLastAccess() == 0 || rep.getLastAccess() < s.getLastSessionRefresh()) { + rep.setLastAccess(s.getLastSessionRefresh()); + } - if (rep.getLastAccess() == 0 || rep.getLastAccess() < s.getLastSessionRefresh()) { - rep.setLastAccess(s.getLastSessionRefresh()); - } - - rep.addSession(createSessionRepresentation(s, device)); - } + rep.addSession(createSessionRepresentation(s, device)); + }); return reps.values(); } @@ -130,13 +129,9 @@ public class SessionResource { @NoCache public Response logout(@QueryParam("current") boolean removeCurrent) { auth.require(AccountRoles.MANAGE_ACCOUNT); - List userSessions = session.sessions().getUserSessions(realm, user); - - for (UserSessionModel s : userSessions) { - if (removeCurrent || !isCurrentSession(s)) { - AuthenticationManager.backchannelLogout(session, s, true); - } - } + session.sessions().getUserSessionsStream(realm, user).filter(s -> removeCurrent || !isCurrentSession(s)) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(s -> AuthenticationManager.backchannelLogout(session, s, true)); return Response.noContent().build(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 567ce955af..1a7eca2f05 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -50,10 +50,8 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponseException; -import org.keycloak.services.clientpolicy.AdminClientRegisterContext; import org.keycloak.services.clientpolicy.AdminClientUpdateContext; import org.keycloak.services.clientpolicy.ClientPolicyException; -import org.keycloak.services.clientpolicy.DefaultClientPolicyManager; import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils; import org.keycloak.services.clientregistration.policy.RegistrationAuth; import org.keycloak.services.managers.ClientManager; @@ -78,11 +76,12 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; import static java.lang.Boolean.TRUE; @@ -462,17 +461,13 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { + public Stream getUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { auth.clients().requireView(client); firstResult = firstResult != null ? firstResult : -1; maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; - List sessions = new ArrayList(); - for (UserSessionModel userSession : session.sessions().getUserSessions(client.getRealm(), client, firstResult, maxResults)) { - UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); - sessions.add(rep); - } - return sessions; + return session.sessions().getUserSessionsStream(client.getRealm(), client, firstResult, maxResults) + .map(ModelToRepresentation::toRepresentation); } /** @@ -511,30 +506,14 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getOfflineUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { + public Stream getOfflineUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { auth.clients().requireView(client); firstResult = firstResult != null ? firstResult : -1; maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; - List sessions = new ArrayList(); - List userSessions = session.sessions().getOfflineUserSessions(client.getRealm(), client, firstResult, maxResults); - for (UserSessionModel userSession : userSessions) { - UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); - // Update lastSessionRefresh with the timestamp from clientSession - for (Map.Entry csEntry : userSession.getAuthenticatedClientSessions().entrySet()) { - String clientUuid = csEntry.getKey(); - AuthenticatedClientSessionModel clientSession = csEntry.getValue(); - - if (client.getId().equals(clientUuid)) { - rep.setLastAccess(Time.toMillis(clientSession.getTimestamp())); - break; - } - } - - sessions.add(rep); - } - return sessions; + return session.sessions().getOfflineUserSessionsStream(client.getRealm(), client, firstResult, maxResults) + .map(this::toUserSessionRepresentation); } /** @@ -701,4 +680,23 @@ public class ClientResource { authorization().disable(); } } + + /** + * Converts the specified {@link UserSessionModel} into a {@link UserSessionRepresentation}. + * + * @param userSession the model to be converted. + * @return a reference to the constructed representation. + */ + private UserSessionRepresentation toUserSessionRepresentation(final UserSessionModel userSession) { + UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); + + // Update lastSessionRefresh with the timestamp from clientSession + Map.Entry result = userSession.getAuthenticatedClientSessions().entrySet().stream() + .filter(entry -> Objects.equals(client.getId(), entry.getKey())) + .findFirst().orElse(null); + if (result != null) { + rep.setLastAccess(Time.toMillis(result.getValue().getTimestamp())); + } + return rep; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 1c5cc64a05..0b3458bac6 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -309,15 +309,9 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getSessions() { + public Stream getSessions() { auth.users().requireView(user); - List sessions = session.sessions().getUserSessions(realm, user); - List reps = new ArrayList(); - for (UserSessionModel session : sessions) { - UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); - reps.add(rep); - } - return reps; + return session.sessions().getUserSessionsStream(realm, user).map(ModelToRepresentation::toRepresentation); } /** @@ -329,30 +323,15 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getOfflineSessions(final @PathParam("clientUuid") String clientUuid) { + public Stream getOfflineSessions(final @PathParam("clientUuid") String clientUuid) { auth.users().requireView(user); ClientModel client = realm.getClientById(clientUuid); if (client == null) { throw new NotFoundException("Client not found"); } - List sessions = new UserSessionManager(session).findOfflineSessions(realm, user); - List reps = new ArrayList(); - for (UserSessionModel session : sessions) { - UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); - - // Update lastSessionRefresh with the timestamp from clientSession - AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessionByClient(clientUuid); - - // Skip if userSession is not for this client - if (clientSession == null) { - continue; - } - - rep.setLastAccess(clientSession.getTimestamp()); - - reps.add(rep); - } - return reps; + return new UserSessionManager(session).findOfflineSessionsStream(realm, user) + .map(session -> toUserSessionRepresentation(session, clientUuid)) + .filter(Objects::nonNull); } /** @@ -503,10 +482,10 @@ public class UserResource { session.users().setNotBeforeForUser(realm, user, Time.currentTime()); - List userSessions = session.sessions().getUserSessions(realm, user); - for (UserSessionModel userSession : userSessions) { - AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true); - } + session.sessions().getUserSessionsStream(realm, user) + .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. + .forEach(userSession -> AuthenticationManager.backchannelLogout(session, realm, userSession, + session.getContext().getUri(), clientConnection, headers, true)); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success(); } @@ -900,4 +879,22 @@ public class UserResource { } } + /** + * Converts the specified {@link UserSessionModel} into a {@link UserSessionRepresentation}. + * + * @param userSession the model to be converted. + * @param clientUuid the client's UUID. + * @return a reference to the constructed representation or {@code null} if the session is not associated with the specified + * client. + */ + private UserSessionRepresentation toUserSessionRepresentation(final UserSessionModel userSession, final String clientUuid) { + UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); + // Update lastSessionRefresh with the timestamp from clientSession + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUuid); + if (clientSession == null) { + return null; + } + rep.setLastAccess(clientSession.getTimestamp()); + return rep; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java index 572f8672e9..aa718c73c9 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java @@ -282,7 +282,7 @@ public class ImpersonationTest extends AbstractKeycloakTest { final UserSessionNotesHolder notesHolder = testingClient.server("test").fetch(session -> { final RealmModel realm = session.realms().getRealmByName("test"); final UserModel user = session.users().getUserById(userId, realm); - final UserSessionModel userSession = session.sessions().getUserSessions(realm, user).get(0); + final UserSessionModel userSession = session.sessions().getUserSessionsStream(realm, user).findFirst().get(); return new UserSessionNotesHolder(userSession.getNotes()); }, UserSessionNotesHolder.class); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerRunOnServerUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerRunOnServerUtil.java index ab47c0f2e0..3ea2533f35 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerRunOnServerUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerRunOnServerUtil.java @@ -135,8 +135,7 @@ final class BrokerRunOnServerUtil { return (session) -> { RealmModel realm = session.realms().getRealmByName("consumer"); UserModel user = session.users().getUserByUsername("testuser", realm); - List userSessions = session.sessions().getUserSessions(realm, user); - UserSessionModel sessions = userSessions.get(0); + UserSessionModel sessions = session.sessions().getUserSessionsStream(realm, user).findFirst().get(); assertEquals("sessionvalue", sessions.getNote("user-session-attr")); }; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java index 45279240bb..38a9e4321f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -42,6 +42,7 @@ import org.keycloak.testsuite.arquillian.annotation.ModelTest; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; @@ -116,7 +117,8 @@ public class UserSessionInitializerTest extends AbstractTestRealmKeycloakTest { assertThat("Count of offline sesions for client 'test-app'", currentSession.sessions().getOfflineSessionsCount(realm, testApp), is((long) 3)); assertThat("Count of offline sesions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1)); - List loadedSessions = currentSession.sessions().getOfflineUserSessions(realm, testApp, 0, 10); + List loadedSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, testApp, 0, 10) + .collect(Collectors.toList()); UserSessionProviderTest.assertSessions(loadedSessions, origSessions); assertSessionLoaded(loadedSessions, origSessions[0].getId(), currentSession.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party"); @@ -166,7 +168,8 @@ public class UserSessionInitializerTest extends AbstractTestRealmKeycloakTest { ClientModel thirdparty = realm.getClientByClientId("third-party"); assertThat("Count of offline sesions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1)); - List loadedSessions = currentSession.sessions().getOfflineUserSessions(realm, thirdparty, 0, 10); + List loadedSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10) + .collect(Collectors.toList()); assertThat("Size of loaded Sessions", loadedSessions.size(), is(1)); assertSessionLoaded(loadedSessions, origSessions[0].getId(), currentSession.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "third-party"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java index a65bed4bd4..296abf1ddd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -102,10 +103,8 @@ public class UserSessionPersisterProviderTest extends AbstractTestRealmKeycloakT // Persist 3 created userSessions and clientSessions as offline RealmModel realm = sessionWL22.realms().getRealm("test"); ClientModel testApp = realm.getClientByClientId("test-app"); - List userSessions = sessionWL22.sessions().getUserSessions(realm, testApp); - for (UserSessionModel userSessionLooper : userSessions) { - persistUserSession(sessionWL22, userSessionLooper, true); - } + sessionWL22.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList()) + .forEach(userSessionLooper -> persistUserSession(sessionWL22, userSessionLooper, true)); }); KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL2) -> { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index f9cda20dbc..969df8b04f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -112,10 +113,8 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes // Key is userSession ID, values are client UUIDS // Persist 3 created userSessions and clientSessions as offline ClientModel testApp = realm.getClientByClientId("test-app"); - List userSessions = currentSession.sessions().getUserSessions(realm, testApp); - for (UserSessionModel userSession : userSessions) { - offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(currentSession, userSession)); - } + currentSession.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList()) + .forEach(userSession -> offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(currentSession, userSession))); }); KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCrud3) -> { @@ -170,7 +169,8 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes Assert.assertEquals(2, currentSession.sessions().getOfflineSessionsCount(realm, testApp)); Assert.assertEquals(1, currentSession.sessions().getOfflineSessionsCount(realm, thirdparty)); - List thirdpartySessions = currentSession.sessions().getOfflineUserSessions(realm, thirdparty, 0, 10); + List thirdpartySessions = currentSession.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10) + .collect(Collectors.toList()); Assert.assertEquals(1, thirdpartySessions.size()); Assert.assertEquals("127.0.0.1", thirdpartySessions.get(0).getIpAddress()); Assert.assertEquals("user1", thirdpartySessions.get(0).getUser().getUsername()); @@ -203,7 +203,8 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes Assert.assertEquals(1, currentSession.sessions().getOfflineSessionsCount(realm, testApp)); Assert.assertEquals(0, currentSession.sessions().getOfflineSessionsCount(realm, thirdparty)); - List testAppSessions = currentSession.sessions().getOfflineUserSessions(realm, testApp, 0, 10); + List testAppSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, testApp, 0, 10) + .collect(Collectors.toList()); Assert.assertEquals(1, testAppSessions.size()); Assert.assertEquals("127.0.0.3", testAppSessions.get(0).getIpAddress()); @@ -462,10 +463,8 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes // Persist 3 created userSessions and clientSessions as offline testApp[0] = realm.getClientByClientId("test-app"); - List userSessions = currentSession.sessions().getUserSessions(realm, testApp[0]); - for (UserSessionModel userSession : userSessions) { - offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(currentSession, userSession)); - } + currentSession.sessions().getUserSessionsStream(realm, testApp[0]).collect(Collectors.toList()) + .forEach(userSession -> offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(currentSession, userSession))); // Assert all previously saved offline sessions found for (Map.Entry> entry : offlineSessions.entrySet()) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index b69409d458..a850384b82 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -46,10 +46,12 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -249,8 +251,10 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { } - assertSessions(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)), sessions[0], sessions[1]); - assertSessions(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)), sessions[2]); + assertSessions(session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername("user1", realm)) + .collect(Collectors.toList()), sessions[0], sessions[1]); + assertSessions(session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername("user2", realm)) + .collect(Collectors.toList()), sessions[2]); } @Test @@ -262,20 +266,18 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { inheritClientConnection(session, kcSession); createSessions(kcSession); }); - Map clientSessionsKept = new HashMap<>(); - for (UserSessionModel s : session.sessions().getUserSessions(realm, - session.users().getUserByUsername("user2", realm))) { - - clientSessionsKept.put(s.getId(), s.getAuthenticatedClientSessions().keySet().size()); - } + Map clientSessionsKept = session.sessions().getUserSessionsStream(realm, + session.users().getUserByUsername("user2", realm)) + .collect(Collectors.toMap(model -> model.getId(), model -> model.getAuthenticatedClientSessions().keySet().size())); KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> { kcSession.sessions().removeUserSessions(realm, kcSession.users().getUserByUsername("user1", realm)); }); - assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); - List userSessions = session.sessions().getUserSessions(realm, - session.users().getUserByUsername("user2", realm)); + assertEquals(0, session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername("user1", realm)) + .count()); + List userSessions = session.sessions().getUserSessionsStream(realm, + session.users().getUserByUsername("user2", realm)).collect(Collectors.toList()); assertSame(userSessions.size(), 1); @@ -309,8 +311,10 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { kcSession.sessions().removeUserSessions(realm); }); - assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); - assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); + assertEquals(0, session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername("user1", realm)) + .count()); + assertEquals(0, session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername("user2", realm)) + .count()); } @Test @@ -544,8 +548,10 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { } - assertSessions(session.sessions().getUserSessions(realm, realm.getClientByClientId("test-app")), sessions[0], sessions[1], sessions[2]); - assertSessions(session.sessions().getUserSessions(realm, realm.getClientByClientId("third-party")), sessions[0]); + assertSessions(session.sessions().getUserSessionsStream(realm, realm.getClientByClientId("test-app")) + .collect(Collectors.toList()), sessions[0], sessions[1], sessions[2]); + assertSessions(session.sessions().getUserSessionsStream(realm, realm.getClientByClientId("third-party")) + .collect(Collectors.toList()), sessions[0]); } @Test @@ -663,7 +669,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { } private static void assertPaginatedSession(KeycloakSession session, RealmModel realm, ClientModel client, int start, int max, int expectedSize) { - List sessions = session.sessions().getUserSessions(realm, client, start, max); + List sessions = session.sessions().getUserSessionsStream(realm, client, start, max).collect(Collectors.toList()); String[] actualIps = new String[sessions.size()]; for (int i = 0; i < actualIps.length; i++) { @@ -773,11 +779,11 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { session.userStorageManager().removeUser(realm, user1); - assertTrue(session.sessions().getUserSessions(realm, user1).isEmpty()); + assertEquals(0, session.sessions().getUserSessionsStream(realm, user1).count()); session.getTransactionManager().commit(); - assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); + assertNotEquals(0, session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername("user2", realm)).count()); user1 = session.users().getUserByUsername("user1", realm); user2 = session.users().getUserByUsername("user2", realm);