From 9c431f3b90441cc6bd77b967f8463dcf64917587 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Mon, 20 Feb 2023 08:59:38 +0100 Subject: [PATCH] introduced combined sessions table needed (offline and regular) (#17023) --- .../admin/ui/rest/AdminExtResource.java | 5 + .../admin/ui/rest/SessionsResource.java | 127 ++++++++++++++++++ .../admin/ui/rest/model/SessionId.java | 41 ++++++ .../ui/rest/model/SessionRepresentation.java | 83 ++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/SessionsResource.java create mode 100644 rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionId.java create mode 100644 rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionRepresentation.java diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java index 82e7dee664..40df71ec61 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java @@ -45,4 +45,9 @@ public final class AdminExtResource { return new GroupsResource(session, realm, auth); } + @Path("/sessions") + public SessionsResource sessions() { + return new SessionsResource(session, realm, auth); + } + } 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 new file mode 100644 index 0000000000..9ff0832be0 --- /dev/null +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/SessionsResource.java @@ -0,0 +1,127 @@ +package org.keycloak.admin.ui.rest; + +import org.eclipse.microprofile.openapi.annotations.Operation; +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.SessionRepresentation; +import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +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.*; + +public class SessionsResource { + private final KeycloakSession session; + private final RealmModel realm; + private final AdminPermissionEvaluator auth; + + public SessionsResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) { + this.session = session; + this.realm = realm; + this.auth = auth; + } + + @GET + @Consumes({"application/json"}) + @Produces({"application/json"}) + @Operation( + summary = "List all sessions of the current realm also the once that use offline tokens", + description = "This endpoint returns a list of sessions and the clients that have been used including offline tokens" + ) + @APIResponse( + responseCode = "200", + description = "", + content = {@Content( + schema = @Schema( + implementation = SessionRepresentation.class, + type = SchemaType.ARRAY + ) + )} + ) + public Stream realmSessions(@QueryParam("type") @DefaultValue("ALL") final SessionType type, + @QueryParam("search") @DefaultValue("") final String search, @QueryParam("first") + @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) { + auth.realm().requireViewRealm(); + + Stream clientIds = Stream.builder().build(); + long clientSessionsCount = 0L; + if (type == ALL || type == REGULAR) { + final Map clientSessionStats = session.sessions().getActiveClientSessionStats(realm, false); + clientSessionsCount = clientSessionStats.values().stream().reduce(0L, Long::sum); + clientIds = Stream.concat(clientIds, clientSessionStats + .keySet().stream().map(i -> new SessionId(i, REGULAR))); + } + if (type == ALL || type == OFFLINE) { + clientIds = Stream.concat(clientIds, session.sessions().getActiveClientSessionStats(realm, true) + .keySet().stream().map(i -> new SessionId(i, OFFLINE))); + } + + + final List sessionIds = clientIds.skip(first).limit(max).collect(Collectors.toList()); + Stream result = Stream.builder().build(); + for (SessionId sessionId : sessionIds) { + ClientModel clientModel = realm.getClientById(sessionId.getClientId()); + switch (sessionId.getType()) { + case REGULAR: + result = Stream.concat(result, session.sessions().getUserSessionsStream(realm, clientModel) + .map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), REGULAR))); + break; + case OFFLINE: + result = Stream.concat(result, session.sessions() + .getOfflineUserSessionsStream(realm, clientModel, Math.max((int) (first - clientSessionsCount), 0), max) + .map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), OFFLINE))); + break; + } + } + + if (!search.equals("")) { + return result.filter(s -> s.getUsername().contains(search) || s.getIpAddress().contains(search)); + } + return result; + } + + 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) { + SessionRepresentation rep = new SessionRepresentation(); + rep.setId(session.getId()); + rep.setStart(Time.toMillis(session.getStarted())); + rep.setLastAccess(Time.toMillis(session.getLastSessionRefresh())); + rep.setUsername(session.getUser().getUsername()); + rep.setUserId(session.getUser().getId()); + rep.setIpAddress(session.getIpAddress()); + rep.setType(type); + for (AuthenticatedClientSessionModel clientSession : session.getAuthenticatedClientSessions().values()) { + ClientModel client = clientSession.getClient(); + rep.getClients().put(client.getId(), client.getClientId()); + } + return rep; + } +} 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/SessionId.java new file mode 100644 index 0000000000..03ca7683dd --- /dev/null +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionId.java @@ -0,0 +1,41 @@ + +package org.keycloak.admin.ui.rest.model; + +import java.util.Objects; + +public class SessionId { + + + public enum SessionType { + ALL, REGULAR, OFFLINE + } + + private final String clientId; + private final SessionType type; + + public SessionId(String clientId, SessionType type) { + this.clientId = clientId; + this.type = type; + } + + public String getClientId() { + return clientId; + } + + public SessionType getType() { + return type; + } + + @Override + 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; + } + + @Override + public int hashCode() { + return Objects.hash(clientId, type); + } +} 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 new file mode 100644 index 0000000000..5dcc81bfa9 --- /dev/null +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/SessionRepresentation.java @@ -0,0 +1,83 @@ +package org.keycloak.admin.ui.rest.model; + +import org.keycloak.admin.ui.rest.model.SessionId.SessionType; + +import java.util.HashMap; +import java.util.Map; + +public class SessionRepresentation { + private String id; + private String username; + private String userId; + private String ipAddress; + private long start; + private long lastAccess; + + private SessionType type; + private Map clients = new HashMap<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public SessionType getType() { + return type; + } + + public void setType(SessionType type) { + this.type = type; + } + + public long getStart() { + return start; + } + + public void setStart(long start) { + this.start = start; + } + + public long getLastAccess() { + return lastAccess; + } + + public void setLastAccess(long lastAccess) { + this.lastAccess = lastAccess; + } + + public Map getClients() { + return clients; + } + + public void setClients(Map clients) { + this.clients = clients; + } +} +