Added role mapping tab to group detail page (#1638)

* feat(group): added role mapping tab

feat(groups): fixed members tests

* Add correct role mapping label

* Use composite group mappings

Co-authored-by: Marco Cesi <sq@iamsquare.it>
This commit is contained in:
alevid99 2021-12-07 13:56:25 +01:00 committed by GitHub
parent e712f72155
commit 51d8038a5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 10 deletions

View file

@ -3,7 +3,8 @@ const expect = chai.expect;
export default class GroupDetailPage { export default class GroupDetailPage {
private groupNamesColumn = '[data-label="Group name"] > a'; private groupNamesColumn = '[data-label="Group name"] > a';
private memberTab = "members"; 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 includeSubGroupsCheck = "includeSubGroupsCheck";
private addMembers = "addMember"; private addMembers = "addMember";
private addMember = "add"; private addMember = "add";

14
package-lock.json generated
View file

@ -7,7 +7,7 @@
"name": "keycloak-admin-ui", "name": "keycloak-admin-ui",
"license": "Apache", "license": "Apache",
"dependencies": { "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/patternfly": "^4.159.1",
"@patternfly/react-code-editor": "^4.16.4", "@patternfly/react-code-editor": "^4.16.4",
"@patternfly/react-core": "^4.175.4", "@patternfly/react-core": "^4.175.4",
@ -3405,9 +3405,9 @@
} }
}, },
"node_modules/@keycloak/keycloak-admin-client": { "node_modules/@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.57", "version": "16.0.0-dev.59",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.57.tgz", "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.59.tgz",
"integrity": "sha512-FErInvNxY4MDpPU4iRf5Sy3M106v/gPgXRWEVVHKpjlH3MG+Ru5QWPiRzZlxeXMCyy1NNyjry9fukQnwOhO0Eg==", "integrity": "sha512-ygDXfVh7MRGbWNA/8zloWh5ULqhukZ+dhptGKuLmN1kxirzsc0P9//96/EYI3FX9rf+xiuF575dkOsR6sQx5Eg==",
"dependencies": { "dependencies": {
"axios": "^0.24.0", "axios": "^0.24.0",
"camelize-ts": "^1.0.8", "camelize-ts": "^1.0.8",
@ -24103,9 +24103,9 @@
} }
}, },
"@keycloak/keycloak-admin-client": { "@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.57", "version": "16.0.0-dev.59",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.57.tgz", "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.59.tgz",
"integrity": "sha512-FErInvNxY4MDpPU4iRf5Sy3M106v/gPgXRWEVVHKpjlH3MG+Ru5QWPiRzZlxeXMCyy1NNyjry9fukQnwOhO0Eg==", "integrity": "sha512-ygDXfVh7MRGbWNA/8zloWh5ULqhukZ+dhptGKuLmN1kxirzsc0P9//96/EYI3FX9rf+xiuF575dkOsR6sQx5Eg==",
"requires": { "requires": {
"axios": "^0.24.0", "axios": "^0.24.0",
"camelize-ts": "^1.0.8", "camelize-ts": "^1.0.8",

View file

@ -23,7 +23,7 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "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/patternfly": "^4.159.1",
"@patternfly/react-code-editor": "^4.16.4", "@patternfly/react-code-editor": "^4.16.4",
"@patternfly/react-core": "^4.175.4", "@patternfly/react-core": "^4.175.4",

View file

@ -23,7 +23,7 @@ import { FilterIcon } from "@patternfly/react-icons";
import { Row, ServiceRole } from "./RoleMapping"; import { Row, ServiceRole } from "./RoleMapping";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; 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 = { type AddRoleMappingModalProps = {
id: string; id: string;
@ -71,6 +71,14 @@ export const AddRoleMappingModal = ({
let roles: RoleRepresentation[] = []; let roles: RoleRepresentation[] = [];
switch (type) { switch (type) {
case "group":
roles =
await adminClient.groups.listAvailableClientRoleMappings({
id: id,
clientUniqueId: client.id!,
});
break;
case "service-account": case "service-account":
roles = await adminClient.users.listAvailableClientRoleMappings( roles = await adminClient.users.listAvailableClientRoleMappings(
{ {
@ -131,6 +139,12 @@ export const AddRoleMappingModal = ({
let availableRoles: RoleRepresentation[] = []; let availableRoles: RoleRepresentation[] = [];
switch (type) { switch (type) {
case "group":
availableRoles =
await adminClient.groups.listAvailableRealmRoleMappings({
id,
});
break;
case "service-account": case "service-account":
availableRoles = await adminClient.users.listAvailableRealmRoleMappings( availableRoles = await adminClient.users.listAvailableRealmRoleMappings(
{ {
@ -168,6 +182,13 @@ export const AddRoleMappingModal = ({
let clientAvailableRoles: RoleRepresentation[] = []; let clientAvailableRoles: RoleRepresentation[] = [];
switch (type) { switch (type) {
case "group":
clientAvailableRoles =
await adminClient.groups.listAvailableClientRoleMappings({
id,
clientUniqueId: client.id!,
});
break;
case "service-account": case "service-account":
clientAvailableRoles = clientAvailableRoles =
await adminClient.users.listAvailableClientRoleMappings({ await adminClient.users.listAvailableClientRoleMappings({

View file

@ -113,6 +113,25 @@ export const RoleMapping = ({
onConfirm: async () => { onConfirm: async () => {
try { try {
switch (type) { 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": case "service-account":
await Promise.all( await Promise.all(
selected.map((row) => { selected.map((row) => {

View file

@ -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 (
<RoleMapping
name={name}
id={id}
type="group"
loader={loader}
save={assignRoles}
onHideRolesToggle={() => setHide(!hide)}
/>
);
};

View file

@ -25,6 +25,7 @@ import { GroupAttributes } from "./GroupAttributes";
import { GroupsModal } from "./GroupsModal"; import { GroupsModal } from "./GroupsModal";
import { toGroups } from "./routes/Groups"; import { toGroups } from "./routes/Groups";
import { toGroupsSearch } from "./routes/GroupsSearch"; import { toGroupsSearch } from "./routes/GroupsSearch";
import { GroupRoleMapping } from "./GroupRoleMapping";
import "./GroupsSection.css"; import "./GroupsSection.css";
@ -166,6 +167,13 @@ export default function GroupsSection() {
> >
<GroupAttributes /> <GroupAttributes />
</Tab> </Tab>
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
>
<GroupRoleMapping id={id!} name={currentGroup().name!} />
</Tab>
</Tabs> </Tabs>
)} )}
{subGroups.length === 0 && <GroupTable />} {subGroups.length === 0 && <GroupTable />}

View file

@ -115,6 +115,7 @@ export const Members = () => {
/> />
)} )}
<KeycloakDataTable <KeycloakDataTable
data-testid="members-table"
key={`${id}${key}${includeSubGroup}`} key={`${id}${key}${includeSubGroup}`}
loader={loader} loader={loader}
ariaLabelKey="groups:members" ariaLabelKey="groups:members"

View file

@ -62,5 +62,6 @@ export default {
attributes: "Attributes", attributes: "Attributes",
groupUpdated: "Group updated", groupUpdated: "Group updated",
groupUpdateError: "Error updating group {error}", groupUpdateError: "Error updating group {error}",
roleMapping: "Role mapping",
}, },
}; };