diff --git a/.github/actions/conditional/conditions b/.github/actions/conditional/conditions index f774c2f9c1e..04e3eb39e84 100644 --- a/.github/actions/conditional/conditions +++ b/.github/actions/conditional/conditions @@ -30,6 +30,7 @@ docs/guides/ guides docs/documentation/ documentation js/ js +rest/admin-ui-ext/ js *.java codeql-java themes/ codeql-themes diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/SessionsResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/SessionsResource.java index cf728806f28..ed7c12bbbd8 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/SessionsResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/SessionsResource.java @@ -5,8 +5,8 @@ import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.keycloak.admin.ui.rest.model.SessionId; -import org.keycloak.admin.ui.rest.model.SessionId.SessionType; +import org.keycloak.admin.ui.rest.model.ClientIdSessionType; +import org.keycloak.admin.ui.rest.model.ClientIdSessionType.SessionType; import org.keycloak.admin.ui.rest.model.SessionRepresentation; import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; @@ -22,14 +22,12 @@ import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import org.keycloak.utils.StringUtil; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.keycloak.admin.ui.rest.model.SessionId.SessionType.*; +import static org.keycloak.admin.ui.rest.model.ClientIdSessionType.SessionType.*; public class SessionsResource { private final KeycloakSession session; @@ -64,49 +62,40 @@ public class SessionsResource { @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) { auth.realm().requireViewRealm(); - Stream sessionIdStream = Stream.builder().build(); + Stream sessionIdStream = Stream.builder().build(); if (type == ALL || type == REGULAR) { final Map clientSessionStats = session.sessions().getActiveClientSessionStats(realm, false); sessionIdStream = Stream.concat(sessionIdStream, clientSessionStats - .keySet().stream().map(i -> new SessionId(i, REGULAR))); + .keySet().stream().map(i -> new ClientIdSessionType(i, REGULAR))); } if (type == ALL || type == OFFLINE) { sessionIdStream = Stream.concat(sessionIdStream, session.sessions().getActiveClientSessionStats(realm, true) - .keySet().stream().map(i -> new SessionId(i, OFFLINE))); + .keySet().stream().map(i -> new ClientIdSessionType(i, OFFLINE))); } - Stream result = sessionIdStream.flatMap((sessionId) -> { - ClientModel clientModel = realm.getClientById(sessionId.getClientId()); - switch (sessionId.getType()) { + Stream result = sessionIdStream.flatMap((clientIdSessionType) -> { + ClientModel clientModel = realm.getClientById(clientIdSessionType.getClientId()); + switch (clientIdSessionType.getType()) { case REGULAR: return session.sessions().getUserSessionsStream(realm, clientModel) - .map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), REGULAR)); + .map(s -> toRepresentation(s, REGULAR)); case OFFLINE: return session.sessions() .getOfflineUserSessionsStream(realm, clientModel, null, null) - .map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), OFFLINE)); + .map(s -> toRepresentation(s, OFFLINE)); } return Stream.builder().build(); - }).distinct(); + }); - if (!search.equals("")) { - result = result.filter(s -> s.getUsername().contains(search) || s.getIpAddress().contains(search) - || s.getClients().values().stream().anyMatch(c -> c.contains(search))); + if (!StringUtil.isBlank(search)) { + String searchTrimmed = search.trim(); + result = result.filter(s -> s.getUsername().contains(searchTrimmed) || s.getIpAddress().contains(searchTrimmed) + || s.getClients().values().stream().anyMatch(c -> c.contains(searchTrimmed))); } - return result.skip(first).limit(max); + return result.distinct().skip(first).limit(max); } - private SessionRepresentation toUserSessionRepresentation(final UserSessionModel userSession, String clientId, SessionType type) { - SessionRepresentation rep = toRepresentation(userSession, type); - - // Update lastSessionRefresh with the timestamp from clientSession - userSession.getAuthenticatedClientSessions().entrySet().stream() - .filter(entry -> Objects.equals(clientId, entry.getKey())) - .findFirst().ifPresent(result -> rep.setLastAccess(Time.toMillis(result.getValue().getTimestamp()))); - return rep; - } - - public static SessionRepresentation toRepresentation(UserSessionModel session, SessionType type) { + private static SessionRepresentation toRepresentation(UserSessionModel session, SessionType type) { SessionRepresentation rep = new SessionRepresentation(); rep.setId(session.getId()); rep.setStart(Time.toMillis(session.getStarted())); diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionId.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/ClientIdSessionType.java similarity index 64% rename from rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionId.java rename to rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/ClientIdSessionType.java index 03ca7683dd1..55ddb2d5fc5 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionId.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/ClientIdSessionType.java @@ -3,7 +3,11 @@ package org.keycloak.admin.ui.rest.model; import java.util.Objects; -public class SessionId { +/** + * A tuple containing the clientId and the session type (online/offline). + * + */ +public class ClientIdSessionType { public enum SessionType { @@ -13,7 +17,7 @@ public class SessionId { private final String clientId; private final SessionType type; - public SessionId(String clientId, SessionType type) { + public ClientIdSessionType(String clientId, SessionType type) { this.clientId = clientId; this.type = type; } @@ -30,8 +34,8 @@ public class SessionId { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SessionId sessionId = (SessionId) o; - return Objects.equals(clientId, sessionId.clientId) && type == sessionId.type; + ClientIdSessionType clientIdSessionType = (ClientIdSessionType) o; + return Objects.equals(clientId, clientIdSessionType.clientId) && type == clientIdSessionType.type; } @Override diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionRepresentation.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionRepresentation.java index 5833f6ae190..c7c1620cfe6 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionRepresentation.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionRepresentation.java @@ -1,6 +1,6 @@ package org.keycloak.admin.ui.rest.model; -import org.keycloak.admin.ui.rest.model.SessionId.SessionType; +import org.keycloak.admin.ui.rest.model.ClientIdSessionType.SessionType; import java.util.HashMap; import java.util.Map; @@ -90,17 +90,20 @@ public class SessionRepresentation { this.transientUser = transientUser; } + /** + * Equality is determined by user session ID and the offline flag, which are also the primary key on this entity. + */ @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof SessionRepresentation)) return false; SessionRepresentation that = (SessionRepresentation) o; - return start == that.start && userId.equals(that.userId) && type == that.type; + return Objects.equals(id, that.id) && type == that.type; } @Override public int hashCode() { - return Objects.hash(userId, start, type); + return Objects.hash(id, type); } }