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:
parent
e712f72155
commit
51d8038a5c
9 changed files with 162 additions and 10 deletions
|
@ -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";
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) => {
|
||||
|
|
101
src/groups/GroupRoleMapping.tsx
Normal file
101
src/groups/GroupRoleMapping.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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() {
|
|||
>
|
||||
<GroupAttributes />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={3}
|
||||
data-testid="role-mapping-tab"
|
||||
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
|
||||
>
|
||||
<GroupRoleMapping id={id!} name={currentGroup().name!} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)}
|
||||
{subGroups.length === 0 && <GroupTable />}
|
||||
|
|
|
@ -115,6 +115,7 @@ export const Members = () => {
|
|||
/>
|
||||
)}
|
||||
<KeycloakDataTable
|
||||
data-testid="members-table"
|
||||
key={`${id}${key}${includeSubGroup}`}
|
||||
loader={loader}
|
||||
ariaLabelKey="groups:members"
|
||||
|
|
|
@ -62,5 +62,6 @@ export default {
|
|||
attributes: "Attributes",
|
||||
groupUpdated: "Group updated",
|
||||
groupUpdateError: "Error updating group {error}",
|
||||
roleMapping: "Role mapping",
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue