diff --git a/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts index 43c36a6f2b..8e34958701 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts @@ -95,20 +95,13 @@ describe("Realm tests", () => { sidebarPage.goToCreateRealm(); createRealmPage.fillRealmName(newRealmName).createRealm(); - const fetchUrl = "/admin/realms?briefRepresentation=true"; - cy.intercept(fetchUrl).as("fetch"); - masthead.checkNotificationMessage("Realm created successfully"); - cy.wait(["@fetch"]); - sidebarPage.goToCreateRealm(); createRealmPage.fillRealmName(editedRealmName).createRealm(); masthead.checkNotificationMessage("Realm created successfully"); - cy.wait(["@fetch"]); - // Show current realms sidebarPage.showCurrentRealms(4); }); diff --git a/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx b/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx index e56907e31e..7b498d7549 100644 --- a/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx +++ b/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx @@ -96,12 +96,8 @@ export const RealmSelector = () => { }) .concat( realms - .filter( - (r) => !recentRealms.includes(r.realm!) || r.realm === realm, - ) - .map((r) => { - return { name: r.realm!, used: false }; - }), + .filter((name) => !recentRealms.includes(name) || name === realm) + .map((name) => ({ name, used: false })), ), [recentRealms, realm, realms], ); @@ -164,15 +160,15 @@ export const RealmSelector = () => { } dropdownItems={(realms.length !== 0 - ? realms.map((r) => ( + ? realms.map((name) => ( setOpen(false)} > - + } /> @@ -187,7 +183,7 @@ export const RealmSelector = () => { {whoAmI.canCreateRealm() && ( <> - + setOpen(false)} /> diff --git a/js/apps/admin-ui/src/context/RealmsContext.tsx b/js/apps/admin-ui/src/context/RealmsContext.tsx index d83b30e1a3..606944ac96 100644 --- a/js/apps/admin-ui/src/context/RealmsContext.tsx +++ b/js/apps/admin-ui/src/context/RealmsContext.tsx @@ -1,16 +1,14 @@ import { NetworkError } from "@keycloak/keycloak-admin-client"; -import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; -import { sortBy } from "lodash-es"; import { PropsWithChildren, useCallback, useMemo, useState } from "react"; import { createNamedContext, useRequiredContext } from "ui-shared"; -import { adminClient } from "../admin-client"; import { keycloak } from "../keycloak"; import { useFetch } from "../utils/useFetch"; +import { fetchAdminUI } from "./auth/admin-ui-endpoint"; type RealmsContextProps = { /** A list of all the realms. */ - realms: RealmRepresentation[]; + realms: string[]; /** Refreshes the realms with the latest information. */ refresh: () => Promise; }; @@ -21,11 +19,11 @@ export const RealmsContext = createNamedContext( ); export const RealmsProvider = ({ children }: PropsWithChildren) => { - const [realms, setRealms] = useState([]); + const [realms, setRealms] = useState([]); const [refreshCount, setRefreshCount] = useState(0); - function updateRealms(realms: RealmRepresentation[]) { - setRealms(sortBy(realms, "realm")); + function updateRealms(realms: string[]) { + setRealms(realms.sort()); } useFetch( @@ -36,7 +34,7 @@ export const RealmsProvider = ({ children }: PropsWithChildren) => { } try { - return await adminClient.realms.find({ briefRepresentation: true }); + return await fetchAdminUI("ui-ext/realms", {}); } catch (error) { if (error instanceof NetworkError && error.response.status < 500) { return []; diff --git a/js/apps/admin-ui/src/context/RecentRealms.tsx b/js/apps/admin-ui/src/context/RecentRealms.tsx index d6262704e8..9d929405ad 100644 --- a/js/apps/admin-ui/src/context/RecentRealms.tsx +++ b/js/apps/admin-ui/src/context/RecentRealms.tsx @@ -1,4 +1,3 @@ -import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; import { PropsWithChildren, useEffect, useMemo } from "react"; import { @@ -44,13 +43,12 @@ export const RecentRealmsProvider = ({ children }: PropsWithChildren) => { export const useRecentRealms = () => useRequiredContext(RecentRealmsContext); -function filterRealmNames(realms: RealmRepresentation[], realmNames: string[]) { +function filterRealmNames(realms: string[], storedRealms: string[]) { // If no realms have been set yet we can't filter out any non-existent realm names. if (realms.length === 0) { - return realmNames; + return storedRealms; } // Only keep realm names that actually still exist. - const exisingRealmNames = realms.map(({ realm }) => realm!); - return realmNames.filter((realm) => exisingRealmNames.includes(realm)); + return storedRealms.filter((realm) => realms.includes(realm)); } 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 6cd7d9f9eb..ba1374e90a 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 @@ -50,4 +50,8 @@ public final class AdminExtResource { return new SessionsResource(session, realm, auth); } + @Path("/realms") + public RealmResource realms() { + return new RealmResource(session); + } } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RealmResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RealmResource.java new file mode 100644 index 0000000000..b64dc07783 --- /dev/null +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RealmResource.java @@ -0,0 +1,49 @@ +package org.keycloak.admin.ui.rest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +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.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.ForbiddenException; + +import java.util.Objects; +import java.util.stream.Stream; + +import static org.keycloak.utils.StreamsUtil.throwIfEmpty; + +public class RealmResource { + private final KeycloakSession session; + + public RealmResource(KeycloakSession session) { + this.session = session; + } + + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Lists only the names of the realms", + description = "Returns a list of realm names based on what the caller is allowed to view" + ) + @APIResponse( + responseCode = "200", + description = "", + content = {@Content( + schema = @Schema( + implementation = String.class, + type = SchemaType.ARRAY + ) + )} + ) + public Stream realmList() { + Stream realms = session.realms().getRealmsStream().filter(Objects::nonNull).map(RealmModel::getName); + return throwIfEmpty(realms, new ForbiddenException()); + } +}