introduced combined sessions table needed (offline and regular) (#17023)

This commit is contained in:
Erik Jan de Wit 2023-02-20 08:59:38 +01:00 committed by GitHub
parent d0828148a2
commit 9c431f3b90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 256 additions and 0 deletions

View file

@ -45,4 +45,9 @@ public final class AdminExtResource {
return new GroupsResource(session, realm, auth); return new GroupsResource(session, realm, auth);
} }
@Path("/sessions")
public SessionsResource sessions() {
return new SessionsResource(session, realm, auth);
}
} }

View file

@ -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<SessionRepresentation> 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<SessionId> clientIds = Stream.<SessionId>builder().build();
long clientSessionsCount = 0L;
if (type == ALL || type == REGULAR) {
final Map<String, Long> 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<SessionId> sessionIds = clientIds.skip(first).limit(max).collect(Collectors.toList());
Stream<SessionRepresentation> result = Stream.<SessionRepresentation>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;
}
}

View file

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

View file

@ -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<String, String> 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<String, String> getClients() {
return clients;
}
public void setClients(Map<String, String> clients) {
this.clients = clients;
}
}