introduced combined sessions table needed (offline and regular) (#17023)
This commit is contained in:
parent
d0828148a2
commit
9c431f3b90
4 changed files with 256 additions and 0 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue