diff --git a/apps/admin-ui/src/components/role-mapping/AddRoleMappingModal.tsx b/apps/admin-ui/src/components/role-mapping/AddRoleMappingModal.tsx index e2c03bdd02..f2fa87f443 100644 --- a/apps/admin-ui/src/components/role-mapping/AddRoleMappingModal.tsx +++ b/apps/admin-ui/src/components/role-mapping/AddRoleMappingModal.tsx @@ -17,7 +17,6 @@ import useLocaleSort from "../../utils/useLocaleSort"; import { ResourcesKey, Row, ServiceRole } from "./RoleMapping"; import { getAvailableRoles } from "./queries"; import { getAvailableClientRoles } from "./resource"; -import { useRealm } from "../../context/realm-context/RealmContext"; type AddRoleMappingModalProps = { id: string; @@ -42,7 +41,6 @@ export const AddRoleMappingModal = ({ }: AddRoleMappingModalProps) => { const { t } = useTranslation("common"); const { adminClient } = useAdminClient(); - const { realm } = useRealm(); const [searchToggle, setSearchToggle] = useState(false); @@ -80,7 +78,6 @@ export const AddRoleMappingModal = ({ const roles = await getAvailableClientRoles({ adminClient, id, - realm, type, first: first || 0, max: max || 10, diff --git a/apps/admin-ui/src/components/role-mapping/RoleMapping.tsx b/apps/admin-ui/src/components/role-mapping/RoleMapping.tsx index 46751020bb..83b0ebb3b1 100644 --- a/apps/admin-ui/src/components/role-mapping/RoleMapping.tsx +++ b/apps/admin-ui/src/components/role-mapping/RoleMapping.tsx @@ -22,7 +22,6 @@ import { useAdminClient } from "../../context/auth/AdminClient"; import { ListEmptyState } from "../list-empty-state/ListEmptyState"; import { deleteMapping, getEffectiveRoles, getMapping } from "./queries"; import { getEffectiveClientRoles } from "./resource"; -import { useRealm } from "../../context/realm-context/RealmContext"; import "./role-mapping.css"; @@ -89,7 +88,6 @@ export const RoleMapping = ({ }: RoleMappingProps) => { const { t } = useTranslation(type); const { adminClient } = useAdminClient(); - const { realm } = useRealm(); const { addAlert, addError } = useAlerts(); const [key, setKey] = useState(0); @@ -113,7 +111,6 @@ export const RoleMapping = ({ effectiveClientRoles = ( await getEffectiveClientRoles({ adminClient, - realm, type, id, }) diff --git a/apps/admin-ui/src/components/role-mapping/resource.ts b/apps/admin-ui/src/components/role-mapping/resource.ts index 4469a049d9..ca7cf2a9c7 100644 --- a/apps/admin-ui/src/components/role-mapping/resource.ts +++ b/apps/admin-ui/src/components/role-mapping/resource.ts @@ -1,11 +1,9 @@ import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; -import { addTrailingSlash } from "../../util"; -import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders"; +import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint"; type BaseClientRolesQuery = { adminClient: KeycloakAdminClient; id: string; - realm: string; type: string; }; @@ -33,29 +31,17 @@ type ClientRole = { const fetchRoles = async ({ adminClient, id, - realm, type, first, max, search, endpoint, }: Query): Promise => { - const accessToken = await adminClient.getAccessToken(); - const baseUrl = adminClient.baseUrl; - - const response = await fetch( - `${addTrailingSlash( - baseUrl - )}admin/realms/${realm}/admin-ui-${endpoint}/${type}/${id}?first=${ - first || 0 - }&max=${max || 10}${search ? "&search=" + search : ""}`, - { - method: "GET", - headers: getAuthorizationHeaders(accessToken), - } - ); - - return await response.json(); + return fetchAdminUI(adminClient, `/admin-ui-${endpoint}/${type}/${id}`, { + first: (first || 0).toString(), + max: (max || 10).toString(), + search: search || "", + }); }; export const getAvailableClientRoles = async ( diff --git a/apps/admin-ui/src/context/auth/admin-ui-endpoint.ts b/apps/admin-ui/src/context/auth/admin-ui-endpoint.ts new file mode 100644 index 0000000000..5b8449a18b --- /dev/null +++ b/apps/admin-ui/src/context/auth/admin-ui-endpoint.ts @@ -0,0 +1,24 @@ +import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; + +import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders"; +import { joinPath } from "../../utils/joinPath"; + +export async function fetchAdminUI( + adminClient: KeycloakAdminClient, + endpoint: string, + query?: Record +) { + const accessToken = await adminClient.getAccessToken(); + const baseUrl = adminClient.baseUrl; + + const response = await fetch( + joinPath(baseUrl, "admin/realms", adminClient.realmName, endpoint) + + (query ? "?" + new URLSearchParams(query) : ""), + { + method: "GET", + headers: getAuthorizationHeaders(accessToken), + } + ); + + return await response.json(); +} diff --git a/apps/admin-ui/src/groups/GroupTable.tsx b/apps/admin-ui/src/groups/GroupTable.tsx index fd12b1802c..3030fa4c21 100644 --- a/apps/admin-ui/src/groups/GroupTable.tsx +++ b/apps/admin-ui/src/groups/GroupTable.tsx @@ -6,6 +6,7 @@ import { cellWidth } from "@patternfly/react-table"; import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation"; import { useAdminClient } from "../context/auth/AdminClient"; +import { fetchAdminUI } from "../context/auth/admin-ui-endpoint"; import { useRealm } from "../context/realm-context/RealmContext"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; @@ -58,9 +59,7 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => { groupsData = group.subGroups; } else { - groupsData = await adminClient.groups.find({ - briefRepresentation: false, - }); + groupsData = await fetchAdminUI(adminClient, "admin-ui-groups"); } if (!groupsData) { diff --git a/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java b/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java new file mode 100644 index 0000000000..5afaead677 --- /dev/null +++ b/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java @@ -0,0 +1,92 @@ +package org.keycloak.admin.ui.rest; + +import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation; + +import java.util.stream.Collectors; +import java.util.stream.Stream; +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 javax.ws.rs.core.Context; +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.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.utils.StringUtil; + +public class GroupsResource { + @Context + private KeycloakSession session; + private RealmModel realm; + private AdminPermissionEvaluator auth; + + public GroupsResource(RealmModel realm, AdminPermissionEvaluator auth) { + super(); + this.realm = realm; + this.auth = auth; + } + + @GET + @Consumes({"application/json"}) + @Produces({"application/json"}) + @Operation( + summary = "List all groups with fine grained authorisation", + description = "This endpoint returns a list of groups with fine grained authorisation" + ) + @APIResponse( + responseCode = "200", + description = "", + content = {@Content( + schema = @Schema( + implementation = GroupRepresentation.class, + type = SchemaType.ARRAY + ) + )} + ) + public final Stream listGroups(@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first") + @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) { + this.auth.groups().requireList(); + final Stream stream; + if ("".equals(search)) { + stream = this.realm.searchForGroupByNameStream(search, first, max); + } else { + stream = this.realm.getTopLevelGroupsStream(first, max); + } + return stream.map(g -> toGroupHierarchy(g, search)); + } + + private GroupRepresentation toGroupHierarchy(GroupModel group, final String search) { + GroupRepresentation rep = toRepresentation(group, true); + rep.setAccess(auth.groups().getAccess(group)); + rep.setSubGroups(group.getSubGroupsStream().filter(g -> + groupMatchesSearchOrIsPathElement( + g, search + ) + ).map(subGroup -> + ModelToRepresentation.toGroupHierarchy( + subGroup, true, search + ) + ).collect(Collectors.toList())); + + return rep; + } + + private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) { + if (StringUtil.isBlank(search)) { + return true; + } + if (group.getName().contains(search)) { + return true; + } + return group.getSubGroupsStream().findAny().isPresent(); + } +} diff --git a/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResourceProvider.java b/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResourceProvider.java new file mode 100644 index 0000000000..ac9ed3172c --- /dev/null +++ b/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResourceProvider.java @@ -0,0 +1,35 @@ +package org.keycloak.admin.ui.rest; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; + +public final class GroupsResourceProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider { + public AdminRealmResourceProvider create(KeycloakSession session) { + return this; + } + + public void init(Config.Scope config) { + + } + + public void postInit(KeycloakSessionFactory factory) { + + } + + public void close() { + } + + public String getId() { + return "admin-ui-groups"; + } + + public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + return new GroupsResource(realm, auth); + } +} diff --git a/keycloak-theme/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory b/keycloak-theme/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory index 15d0536fc0..bfcf8c0c4b 100644 --- a/keycloak-theme/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory +++ b/keycloak-theme/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory @@ -16,4 +16,5 @@ # org.keycloak.admin.ui.rest.AvailableRoleMappingProvider -org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider \ No newline at end of file +org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider +org.keycloak.admin.ui.rest.GroupsResourceProvider \ No newline at end of file