diff --git a/cypress/support/pages/admin_console/manage/groups/GroupDetailPage.ts b/cypress/support/pages/admin_console/manage/groups/GroupDetailPage.ts index ab8ef2e2b1..7197d84cf1 100644 --- a/cypress/support/pages/admin_console/manage/groups/GroupDetailPage.ts +++ b/cypress/support/pages/admin_console/manage/groups/GroupDetailPage.ts @@ -3,7 +3,8 @@ const expect = chai.expect; export default class GroupDetailPage { private groupNamesColumn = '[data-label="Group name"] > a'; private memberTab = "members"; - private memberNameColumn = 'tbody > tr > [data-label="Name"]'; + private memberNameColumn = + '[data-testid="members-table"] > tbody > tr > [data-label="Name"]'; private includeSubGroupsCheck = "includeSubGroupsCheck"; private addMembers = "addMember"; private addMember = "add"; diff --git a/package-lock.json b/package-lock.json index 92cf487fd8..2b29d8554e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "keycloak-admin-ui", "license": "Apache", "dependencies": { - "@keycloak/keycloak-admin-client": "^16.0.0-dev.57", + "@keycloak/keycloak-admin-client": "^16.0.0-dev.59", "@patternfly/patternfly": "^4.159.1", "@patternfly/react-code-editor": "^4.16.4", "@patternfly/react-core": "^4.175.4", @@ -3405,9 +3405,9 @@ } }, "node_modules/@keycloak/keycloak-admin-client": { - "version": "16.0.0-dev.57", - "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.57.tgz", - "integrity": "sha512-FErInvNxY4MDpPU4iRf5Sy3M106v/gPgXRWEVVHKpjlH3MG+Ru5QWPiRzZlxeXMCyy1NNyjry9fukQnwOhO0Eg==", + "version": "16.0.0-dev.59", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.59.tgz", + "integrity": "sha512-ygDXfVh7MRGbWNA/8zloWh5ULqhukZ+dhptGKuLmN1kxirzsc0P9//96/EYI3FX9rf+xiuF575dkOsR6sQx5Eg==", "dependencies": { "axios": "^0.24.0", "camelize-ts": "^1.0.8", @@ -24103,9 +24103,9 @@ } }, "@keycloak/keycloak-admin-client": { - "version": "16.0.0-dev.57", - "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.57.tgz", - "integrity": "sha512-FErInvNxY4MDpPU4iRf5Sy3M106v/gPgXRWEVVHKpjlH3MG+Ru5QWPiRzZlxeXMCyy1NNyjry9fukQnwOhO0Eg==", + "version": "16.0.0-dev.59", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.59.tgz", + "integrity": "sha512-ygDXfVh7MRGbWNA/8zloWh5ULqhukZ+dhptGKuLmN1kxirzsc0P9//96/EYI3FX9rf+xiuF575dkOsR6sQx5Eg==", "requires": { "axios": "^0.24.0", "camelize-ts": "^1.0.8", diff --git a/package.json b/package.json index dfb045407f..6ef497c087 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prepare": "husky install" }, "dependencies": { - "@keycloak/keycloak-admin-client": "^16.0.0-dev.57", + "@keycloak/keycloak-admin-client": "^16.0.0-dev.59", "@patternfly/patternfly": "^4.159.1", "@patternfly/react-code-editor": "^4.16.4", "@patternfly/react-core": "^4.175.4", diff --git a/src/components/role-mapping/AddRoleMappingModal.tsx b/src/components/role-mapping/AddRoleMappingModal.tsx index 105c2239fd..23cf22e5ee 100644 --- a/src/components/role-mapping/AddRoleMappingModal.tsx +++ b/src/components/role-mapping/AddRoleMappingModal.tsx @@ -23,7 +23,7 @@ import { FilterIcon } from "@patternfly/react-icons"; import { Row, ServiceRole } from "./RoleMapping"; import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; -export type MappingType = "service-account" | "client-scope" | "role"; +export type MappingType = "service-account" | "client-scope" | "role" | "group"; type AddRoleMappingModalProps = { id: string; @@ -71,6 +71,14 @@ export const AddRoleMappingModal = ({ let roles: RoleRepresentation[] = []; switch (type) { + case "group": + roles = + await adminClient.groups.listAvailableClientRoleMappings({ + id: id, + clientUniqueId: client.id!, + }); + break; + case "service-account": roles = await adminClient.users.listAvailableClientRoleMappings( { @@ -131,6 +139,12 @@ export const AddRoleMappingModal = ({ let availableRoles: RoleRepresentation[] = []; switch (type) { + case "group": + availableRoles = + await adminClient.groups.listAvailableRealmRoleMappings({ + id, + }); + break; case "service-account": availableRoles = await adminClient.users.listAvailableRealmRoleMappings( { @@ -168,6 +182,13 @@ export const AddRoleMappingModal = ({ let clientAvailableRoles: RoleRepresentation[] = []; switch (type) { + case "group": + clientAvailableRoles = + await adminClient.groups.listAvailableClientRoleMappings({ + id, + clientUniqueId: client.id!, + }); + break; case "service-account": clientAvailableRoles = await adminClient.users.listAvailableClientRoleMappings({ diff --git a/src/components/role-mapping/RoleMapping.tsx b/src/components/role-mapping/RoleMapping.tsx index d293ca0530..f3b74c0749 100644 --- a/src/components/role-mapping/RoleMapping.tsx +++ b/src/components/role-mapping/RoleMapping.tsx @@ -113,6 +113,25 @@ export const RoleMapping = ({ onConfirm: async () => { try { switch (type) { + case "group": + await Promise.all( + selected.map((row) => { + const role = { id: row.role.id!, name: row.role.name! }; + if (row.client) { + return adminClient.groups.delClientRoleMappings({ + id, + clientUniqueId: row.client!.id!, + roles: [role], + }); + } else { + return adminClient.groups.delRealmRoleMappings({ + id, + roles: [role], + }); + } + }) + ); + break; case "service-account": await Promise.all( selected.map((row) => { diff --git a/src/groups/GroupRoleMapping.tsx b/src/groups/GroupRoleMapping.tsx new file mode 100644 index 0000000000..6e4a7c133e --- /dev/null +++ b/src/groups/GroupRoleMapping.tsx @@ -0,0 +1,101 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AlertVariant } from "@patternfly/react-core"; + +import type { RoleMappingPayload } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { useAlerts } from "../components/alert/Alerts"; +import { + mapRoles, + RoleMapping, + Row, +} from "../components/role-mapping/RoleMapping"; + +type GroupRoleMappingProps = { + id: string; + name: string; +}; + +export const GroupRoleMapping = ({ id, name }: GroupRoleMappingProps) => { + const { t } = useTranslation("clients"); + const adminClient = useAdminClient(); + const { addAlert, addError } = useAlerts(); + + const [hide, setHide] = useState(false); + + const loader = async () => { + const [assignedRoles, effectiveRoles] = await Promise.all([ + adminClient.groups + .listRealmRoleMappings({ id }) + .then((roles) => roles.map((role) => ({ role }))), + adminClient.groups + .listCompositeRealmRoleMappings({ id }) + .then((roles) => roles.map((role) => ({ role }))), + ]); + + const clients = await adminClient.clients.find(); + const clientRoles = ( + await Promise.all( + clients.map(async (client) => { + const [clientAssignedRoles, clientEffectiveRoles] = await Promise.all( + [ + adminClient.groups + .listClientRoleMappings({ + id, + clientUniqueId: client.id!, + }) + .then((roles) => roles.map((role) => ({ role, client }))), + adminClient.groups + .listCompositeClientRoleMappings({ + id, + clientUniqueId: client.id!, + }) + .then((roles) => roles.map((role) => ({ role, client }))), + ] + ); + return mapRoles(clientAssignedRoles, clientEffectiveRoles, hide); + }) + ) + ).flat(); + + return [...mapRoles(assignedRoles, effectiveRoles, hide), ...clientRoles]; + }; + + const assignRoles = async (rows: Row[]) => { + try { + const realmRoles = rows + .filter((row) => row.client === undefined) + .map((row) => row.role as RoleMappingPayload) + .flat(); + await adminClient.groups.addRealmRoleMappings({ + id, + roles: realmRoles, + }); + await Promise.all( + rows + .filter((row) => row.client !== undefined) + .map((row) => + adminClient.groups.addClientRoleMappings({ + id, + clientUniqueId: row.client!.id!, + roles: [row.role as RoleMappingPayload], + }) + ) + ); + addAlert(t("roleMappingUpdatedSuccess"), AlertVariant.success); + } catch (error) { + addError("clients:roleMappingUpdatedError", error); + } + }; + + return ( + setHide(!hide)} + /> + ); +}; diff --git a/src/groups/GroupsSection.tsx b/src/groups/GroupsSection.tsx index 375e84decb..d5191968a3 100644 --- a/src/groups/GroupsSection.tsx +++ b/src/groups/GroupsSection.tsx @@ -25,6 +25,7 @@ import { GroupAttributes } from "./GroupAttributes"; import { GroupsModal } from "./GroupsModal"; import { toGroups } from "./routes/Groups"; import { toGroupsSearch } from "./routes/GroupsSearch"; +import { GroupRoleMapping } from "./GroupRoleMapping"; import "./GroupsSection.css"; @@ -166,6 +167,13 @@ export default function GroupsSection() { > + {t("roleMapping")}} + > + + )} {subGroups.length === 0 && } diff --git a/src/groups/Members.tsx b/src/groups/Members.tsx index 2ef80c396b..87c8c40c58 100644 --- a/src/groups/Members.tsx +++ b/src/groups/Members.tsx @@ -115,6 +115,7 @@ export const Members = () => { /> )}