Rework the result for the session search to contain a single result per user sessions

Closes #29203

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-04-22 13:23:29 +02:00 committed by Alexander Schwartz
parent 82a959fa78
commit df47dee924
4 changed files with 34 additions and 37 deletions

View file

@ -30,6 +30,7 @@ docs/guides/ guides
docs/documentation/ documentation
js/ js
rest/admin-ui-ext/ js
*.java codeql-java
themes/ codeql-themes

View file

@ -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<SessionId> sessionIdStream = Stream.<SessionId>builder().build();
Stream<ClientIdSessionType> sessionIdStream = Stream.<ClientIdSessionType>builder().build();
if (type == ALL || type == REGULAR) {
final Map<String, Long> 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<SessionRepresentation> result = sessionIdStream.flatMap((sessionId) -> {
ClientModel clientModel = realm.getClientById(sessionId.getClientId());
switch (sessionId.getType()) {
Stream<SessionRepresentation> 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.<SessionRepresentation>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()));

View file

@ -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

View file

@ -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);
}
}