added new endpoint that concatenates offline and regular sessions for clients (#36914)

fixes: #36596

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2025-02-04 21:48:12 +01:00 committed by GitHub
parent e53a56317e
commit 0e1f1c69af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 59 additions and 24 deletions

View File

@ -3,7 +3,7 @@ import type UserSessionRepresentation from "@keycloak/keycloak-admin-client/lib/
import { PageSection } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client";
import type { LoaderFunction } from "@keycloak/keycloak-ui-shared";
import { fetchAdminUI } from "../context/auth/admin-ui-endpoint";
import SessionsTable from "../sessions/SessionsTable";
type ClientSessionsProps = {
@ -15,27 +15,18 @@ export const ClientSessions = ({ client }: ClientSessionsProps) => {
const { t } = useTranslation();
const loader: LoaderFunction<UserSessionRepresentation> = async () => {
const mapSessionsToType =
(type: string) => (sessions: UserSessionRepresentation[]) =>
sessions.map((session) => ({
type,
...session,
}));
const allSessions = await Promise.all([
adminClient.clients
.listSessions({ id: client.id! })
.then(mapSessionsToType(t("sessionsType.regularSSO"))),
adminClient.clients
.listOfflineSessions({
id: client.id!,
})
.then(mapSessionsToType(t("sessionsType.offline"))),
]);
return allSessions.flat();
};
const loader = async (first?: number, max?: number, search?: string) =>
fetchAdminUI<UserSessionRepresentation[]>(
adminClient,
"ui-ext/sessions/client",
{
first: `${first}`,
max: `${max}`,
type: "ALL",
clientId: client.id!,
search: search || "",
},
);
return (
<PageSection variant="light" className="pf-v5-u-p-0">

View File

@ -1,5 +1,6 @@
package org.keycloak.admin.ui.rest;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
@ -91,12 +92,55 @@ public class SessionsResource {
return Stream.<SessionRepresentation>builder().build();
});
return applySearch(search, result).distinct().skip(first).limit(max);
}
@GET
@Path("client")
@Consumes({"application/json"})
@Produces({"application/json"})
@Operation(
summary = "List all sessions of the passed client containing regular and offline",
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> clientSessions(@QueryParam("clientId") final String clientId,
@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) {
ClientModel clientModel = realm.getClientById(clientId);
auth.clients().requireView(clientModel);
Stream<SessionRepresentation> result = Stream.<SessionRepresentation>builder().build();
if (type == ALL || type == REGULAR) {
result = Stream.concat(result, session.sessions()
.getUserSessionsStream(clientModel.getRealm(), clientModel).map(s -> toRepresentation(s, REGULAR)));
}
if (type == ALL || type == OFFLINE) {
result = Stream.concat(result, session.sessions()
.getOfflineUserSessionsStream(clientModel.getRealm(), clientModel, null, null)
.map(s -> toRepresentation(s, OFFLINE)));
}
return applySearch(search, result).distinct().skip(first).limit(max);
}
private static Stream<SessionRepresentation> applySearch(String search, Stream<SessionRepresentation> result) {
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)));
|| s.getClients().values().stream().anyMatch(c -> c.contains(searchTrimmed)));
}
return result.distinct().skip(first).limit(max);
return result;
}
private static SessionRepresentation toRepresentation(UserSessionModel session, SessionType type) {