added group endpoint that also returns access (#3170)
* added group endpoint that also returns access fixes: #3163 * convert to java * fixed merge errors
This commit is contained in:
parent
df072f5c15
commit
93b0144ff3
8 changed files with 161 additions and 30 deletions
|
@ -17,7 +17,6 @@ import useLocaleSort from "../../utils/useLocaleSort";
|
||||||
import { ResourcesKey, Row, ServiceRole } from "./RoleMapping";
|
import { ResourcesKey, Row, ServiceRole } from "./RoleMapping";
|
||||||
import { getAvailableRoles } from "./queries";
|
import { getAvailableRoles } from "./queries";
|
||||||
import { getAvailableClientRoles } from "./resource";
|
import { getAvailableClientRoles } from "./resource";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
|
||||||
|
|
||||||
type AddRoleMappingModalProps = {
|
type AddRoleMappingModalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -42,7 +41,6 @@ export const AddRoleMappingModal = ({
|
||||||
}: AddRoleMappingModalProps) => {
|
}: AddRoleMappingModalProps) => {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const { adminClient } = useAdminClient();
|
const { adminClient } = useAdminClient();
|
||||||
const { realm } = useRealm();
|
|
||||||
|
|
||||||
const [searchToggle, setSearchToggle] = useState(false);
|
const [searchToggle, setSearchToggle] = useState(false);
|
||||||
|
|
||||||
|
@ -80,7 +78,6 @@ export const AddRoleMappingModal = ({
|
||||||
const roles = await getAvailableClientRoles({
|
const roles = await getAvailableClientRoles({
|
||||||
adminClient,
|
adminClient,
|
||||||
id,
|
id,
|
||||||
realm,
|
|
||||||
type,
|
type,
|
||||||
first: first || 0,
|
first: first || 0,
|
||||||
max: max || 10,
|
max: max || 10,
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
||||||
import { deleteMapping, getEffectiveRoles, getMapping } from "./queries";
|
import { deleteMapping, getEffectiveRoles, getMapping } from "./queries";
|
||||||
import { getEffectiveClientRoles } from "./resource";
|
import { getEffectiveClientRoles } from "./resource";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
|
||||||
|
|
||||||
import "./role-mapping.css";
|
import "./role-mapping.css";
|
||||||
|
|
||||||
|
@ -89,7 +88,6 @@ export const RoleMapping = ({
|
||||||
}: RoleMappingProps) => {
|
}: RoleMappingProps) => {
|
||||||
const { t } = useTranslation(type);
|
const { t } = useTranslation(type);
|
||||||
const { adminClient } = useAdminClient();
|
const { adminClient } = useAdminClient();
|
||||||
const { realm } = useRealm();
|
|
||||||
const { addAlert, addError } = useAlerts();
|
const { addAlert, addError } = useAlerts();
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
|
@ -113,7 +111,6 @@ export const RoleMapping = ({
|
||||||
effectiveClientRoles = (
|
effectiveClientRoles = (
|
||||||
await getEffectiveClientRoles({
|
await getEffectiveClientRoles({
|
||||||
adminClient,
|
adminClient,
|
||||||
realm,
|
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||||
import { addTrailingSlash } from "../../util";
|
import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
|
||||||
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
|
|
||||||
|
|
||||||
type BaseClientRolesQuery = {
|
type BaseClientRolesQuery = {
|
||||||
adminClient: KeycloakAdminClient;
|
adminClient: KeycloakAdminClient;
|
||||||
id: string;
|
id: string;
|
||||||
realm: string;
|
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,29 +31,17 @@ type ClientRole = {
|
||||||
const fetchRoles = async ({
|
const fetchRoles = async ({
|
||||||
adminClient,
|
adminClient,
|
||||||
id,
|
id,
|
||||||
realm,
|
|
||||||
type,
|
type,
|
||||||
first,
|
first,
|
||||||
max,
|
max,
|
||||||
search,
|
search,
|
||||||
endpoint,
|
endpoint,
|
||||||
}: Query): Promise<ClientRole[]> => {
|
}: Query): Promise<ClientRole[]> => {
|
||||||
const accessToken = await adminClient.getAccessToken();
|
return fetchAdminUI(adminClient, `/admin-ui-${endpoint}/${type}/${id}`, {
|
||||||
const baseUrl = adminClient.baseUrl;
|
first: (first || 0).toString(),
|
||||||
|
max: (max || 10).toString(),
|
||||||
const response = await fetch(
|
search: search || "",
|
||||||
`${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();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableClientRoles = async (
|
export const getAvailableClientRoles = async (
|
||||||
|
|
24
apps/admin-ui/src/context/auth/admin-ui-endpoint.ts
Normal file
24
apps/admin-ui/src/context/auth/admin-ui-endpoint.ts
Normal file
|
@ -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();
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { cellWidth } from "@patternfly/react-table";
|
||||||
|
|
||||||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
|
import { fetchAdminUI } from "../context/auth/admin-ui-endpoint";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
|
@ -58,9 +59,7 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
|
|
||||||
groupsData = group.subGroups;
|
groupsData = group.subGroups;
|
||||||
} else {
|
} else {
|
||||||
groupsData = await adminClient.groups.find({
|
groupsData = await fetchAdminUI(adminClient, "admin-ui-groups");
|
||||||
briefRepresentation: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupsData) {
|
if (!groupsData) {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,3 +17,4 @@
|
||||||
|
|
||||||
org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
|
org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
|
||||||
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider
|
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider
|
||||||
|
org.keycloak.admin.ui.rest.GroupsResourceProvider
|
Loading…
Reference in a new issue