Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

added group endpoint that also returns access #3170

Merged
merged 3 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,7 +41,6 @@ export const AddRoleMappingModal = ({
}: AddRoleMappingModalProps) => {
const { t } = useTranslation("common");
const { adminClient } = useAdminClient();
const { realm } = useRealm();

const [searchToggle, setSearchToggle] = useState(false);

Expand Down Expand Up @@ -80,7 +78,6 @@ export const AddRoleMappingModal = ({
const roles = await getAvailableClientRoles({
adminClient,
id,
realm,
type,
first: first || 0,
max: max || 10,
Expand Down
3 changes: 0 additions & 3 deletions apps/admin-ui/src/components/role-mapping/RoleMapping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand All @@ -113,7 +111,6 @@ export const RoleMapping = ({
effectiveClientRoles = (
await getEffectiveClientRoles({
adminClient,
realm,
type,
id,
})
Expand Down
26 changes: 6 additions & 20 deletions apps/admin-ui/src/components/role-mapping/resource.ts
Original file line number Diff line number Diff line change
@@ -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;
};

Expand Down Expand Up @@ -33,29 +31,17 @@ type ClientRole = {
const fetchRoles = async ({
adminClient,
id,
realm,
type,
first,
max,
search,
endpoint,
}: Query): Promise<ClientRole[]> => {
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 (
Expand Down
24 changes: 24 additions & 0 deletions apps/admin-ui/src/context/auth/admin-ui-endpoint.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>
) {
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();
}
5 changes: 2 additions & 3 deletions apps/admin-ui/src/groups/GroupTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GroupRepresentation> 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<GroupModel> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
#

org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider
org.keycloak.admin.ui.rest.GroupsResourceProvider