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 {
|
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
14
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
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 { 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 />}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue