2021-04-20 12:10:00 +00:00
|
|
|
import React, { useState } from "react";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import {
|
|
|
|
AlertVariant,
|
|
|
|
Badge,
|
|
|
|
Button,
|
|
|
|
ButtonVariant,
|
|
|
|
Checkbox,
|
|
|
|
ToolbarItem,
|
|
|
|
} from "@patternfly/react-core";
|
2022-04-29 07:03:39 +00:00
|
|
|
import { cellWidth } from "@patternfly/react-table";
|
2021-04-20 12:10:00 +00:00
|
|
|
|
2021-08-26 08:39:35 +00:00
|
|
|
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
|
|
|
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
2022-02-21 12:14:20 +00:00
|
|
|
import type { ClientScopes } from "@keycloak/keycloak-admin-client/lib/resources/clientScopes";
|
|
|
|
import type { Groups } from "@keycloak/keycloak-admin-client/lib/resources/groups";
|
|
|
|
import type { Roles } from "@keycloak/keycloak-admin-client/lib/resources/roles";
|
|
|
|
import type { Clients } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
|
|
|
import type KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
|
|
|
import { AddRoleMappingModal } from "./AddRoleMappingModal";
|
2021-04-20 12:10:00 +00:00
|
|
|
import { KeycloakDataTable } from "../table-toolbar/KeycloakDataTable";
|
2022-05-02 05:51:09 +00:00
|
|
|
import { emptyFormatter, upperCaseFormatter } from "../../util";
|
2022-02-21 12:14:20 +00:00
|
|
|
import { useAlerts } from "../alert/Alerts";
|
2021-04-20 12:10:00 +00:00
|
|
|
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
|
|
|
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
2021-06-16 11:35:03 +00:00
|
|
|
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
2021-04-20 12:10:00 +00:00
|
|
|
|
2022-02-21 12:14:20 +00:00
|
|
|
import "./role-mapping.css";
|
|
|
|
|
2021-04-20 12:10:00 +00:00
|
|
|
export type CompositeRole = RoleRepresentation & {
|
|
|
|
parent: RoleRepresentation;
|
2021-06-15 06:14:38 +00:00
|
|
|
isInherited?: boolean;
|
2021-04-20 12:10:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export type Row = {
|
|
|
|
client?: ClientRepresentation;
|
2021-06-15 06:14:38 +00:00
|
|
|
role: RoleRepresentation | CompositeRole;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const mapRoles = (
|
2021-06-15 11:12:32 +00:00
|
|
|
assignedRoles: Row[],
|
|
|
|
effectiveRoles: Row[],
|
2021-06-15 06:14:38 +00:00
|
|
|
hide: boolean
|
2022-04-29 07:03:39 +00:00
|
|
|
) => [
|
|
|
|
...(hide
|
|
|
|
? assignedRoles.map((row) => ({
|
|
|
|
...row,
|
|
|
|
role: {
|
|
|
|
...row.role,
|
|
|
|
isInherited: false,
|
|
|
|
},
|
|
|
|
}))
|
|
|
|
: effectiveRoles.map((row) => ({
|
|
|
|
...row,
|
|
|
|
role: {
|
|
|
|
...row.role,
|
|
|
|
isInherited:
|
|
|
|
assignedRoles.find((r) => r.role.id === row.role.id) === undefined,
|
|
|
|
},
|
|
|
|
}))),
|
|
|
|
];
|
2021-04-20 12:10:00 +00:00
|
|
|
|
|
|
|
export const ServiceRole = ({ role, client }: Row) => (
|
|
|
|
<>
|
|
|
|
{client && (
|
2022-05-02 05:51:09 +00:00
|
|
|
<Badge isRead className="keycloak-admin--role-mapping__client-name">
|
2021-04-20 12:10:00 +00:00
|
|
|
{client.clientId}
|
|
|
|
</Badge>
|
|
|
|
)}
|
|
|
|
{role.name}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
2022-02-21 12:14:20 +00:00
|
|
|
export type ResourcesKey = keyof KeycloakAdminClient;
|
|
|
|
|
2021-04-20 12:10:00 +00:00
|
|
|
type RoleMappingProps = {
|
|
|
|
name: string;
|
|
|
|
id: string;
|
2022-02-21 12:14:20 +00:00
|
|
|
type: ResourcesKey;
|
2021-04-20 12:10:00 +00:00
|
|
|
loader: () => Promise<Row[]>;
|
|
|
|
save: (rows: Row[]) => Promise<void>;
|
|
|
|
onHideRolesToggle: () => void;
|
|
|
|
};
|
|
|
|
|
2022-02-21 12:14:20 +00:00
|
|
|
type DeleteFunctions =
|
|
|
|
| keyof Pick<Groups, "delClientRoleMappings" | "delRealmRoleMappings">
|
|
|
|
| keyof Pick<
|
|
|
|
ClientScopes,
|
|
|
|
"delClientScopeMappings" | "delRealmScopeMappings"
|
|
|
|
>;
|
|
|
|
|
|
|
|
type ListFunction =
|
|
|
|
| keyof Pick<
|
|
|
|
Groups,
|
|
|
|
"listAvailableClientRoleMappings" | "listAvailableRealmRoleMappings"
|
|
|
|
>
|
|
|
|
| keyof Pick<
|
|
|
|
ClientScopes,
|
|
|
|
"listAvailableClientScopeMappings" | "listAvailableRealmScopeMappings"
|
|
|
|
>
|
|
|
|
| keyof Pick<Roles, "find">
|
|
|
|
| keyof Pick<Clients, "listRoles">;
|
|
|
|
|
|
|
|
type FunctionMapping = { delete: DeleteFunctions[]; list: ListFunction[] };
|
|
|
|
|
|
|
|
type ResourceMapping = {
|
|
|
|
resource: ResourcesKey;
|
|
|
|
functions: FunctionMapping;
|
|
|
|
};
|
|
|
|
|
|
|
|
const groupFunctions: FunctionMapping = {
|
|
|
|
delete: ["delClientRoleMappings", "delRealmRoleMappings"],
|
|
|
|
list: ["listAvailableClientRoleMappings", "listAvailableRealmRoleMappings"],
|
|
|
|
};
|
|
|
|
|
|
|
|
const clientFunctions: FunctionMapping = {
|
|
|
|
delete: ["delClientScopeMappings", "delRealmScopeMappings"],
|
|
|
|
list: ["listAvailableClientScopeMappings", "listAvailableRealmScopeMappings"],
|
|
|
|
};
|
|
|
|
|
|
|
|
export const mapping: ResourceMapping[] = [
|
|
|
|
{
|
|
|
|
resource: "groups",
|
|
|
|
functions: groupFunctions,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
resource: "users",
|
|
|
|
functions: groupFunctions,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
resource: "clientScopes",
|
|
|
|
functions: clientFunctions,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
resource: "clients",
|
|
|
|
functions: clientFunctions,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
resource: "roles",
|
|
|
|
functions: {
|
|
|
|
delete: [],
|
|
|
|
list: ["listRoles", "find"],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
export const castAdminClient = (
|
|
|
|
adminClient: KeycloakAdminClient,
|
|
|
|
resource: ResourcesKey
|
|
|
|
) =>
|
|
|
|
adminClient[resource] as unknown as {
|
|
|
|
[index in DeleteFunctions | ListFunction]: (
|
|
|
|
...params: any
|
|
|
|
) => Promise<RoleRepresentation[]>;
|
|
|
|
};
|
|
|
|
|
2021-04-20 12:10:00 +00:00
|
|
|
export const RoleMapping = ({
|
|
|
|
name,
|
|
|
|
id,
|
|
|
|
type,
|
|
|
|
loader,
|
|
|
|
save,
|
|
|
|
onHideRolesToggle,
|
|
|
|
}: RoleMappingProps) => {
|
2022-04-05 15:02:27 +00:00
|
|
|
const { t } = useTranslation(type);
|
2021-04-20 12:10:00 +00:00
|
|
|
const adminClient = useAdminClient();
|
2021-07-28 12:01:42 +00:00
|
|
|
const { addAlert, addError } = useAlerts();
|
2021-04-20 12:10:00 +00:00
|
|
|
|
|
|
|
const [key, setKey] = useState(0);
|
2022-02-21 12:14:20 +00:00
|
|
|
const refresh = () => setKey(key + 1);
|
2021-04-20 12:10:00 +00:00
|
|
|
|
|
|
|
const [hide, setHide] = useState(false);
|
|
|
|
const [showAssign, setShowAssign] = useState(false);
|
|
|
|
const [selected, setSelected] = useState<Row[]>([]);
|
|
|
|
|
|
|
|
const assignRoles = async (rows: Row[]) => {
|
|
|
|
await save(rows);
|
|
|
|
refresh();
|
|
|
|
};
|
|
|
|
|
|
|
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
|
|
|
titleKey: "clients:removeMappingTitle",
|
2022-04-05 15:02:27 +00:00
|
|
|
messageKey: t("clients:removeMappingConfirm", { count: selected.length }),
|
2021-04-20 12:10:00 +00:00
|
|
|
continueButtonLabel: "common:remove",
|
|
|
|
continueButtonVariant: ButtonVariant.danger,
|
|
|
|
onConfirm: async () => {
|
|
|
|
try {
|
2022-02-21 12:14:20 +00:00
|
|
|
const mapType = mapping.find((m) => m.resource === type)!;
|
|
|
|
await Promise.all(
|
|
|
|
selected.map((row) => {
|
|
|
|
const role = { id: row.role.id!, name: row.role.name! };
|
|
|
|
castAdminClient(adminClient, mapType.resource)[
|
|
|
|
mapType.functions.delete[row.client ? 0 : 1]
|
|
|
|
](
|
|
|
|
{
|
|
|
|
id,
|
|
|
|
clientUniqueId: row.client?.id,
|
|
|
|
client: row.client?.id,
|
|
|
|
roles: [role],
|
|
|
|
},
|
|
|
|
[role]
|
2021-06-02 21:20:38 +00:00
|
|
|
);
|
2022-02-21 12:14:20 +00:00
|
|
|
})
|
|
|
|
);
|
2021-04-20 12:10:00 +00:00
|
|
|
addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success);
|
|
|
|
refresh();
|
|
|
|
} catch (error) {
|
2021-07-28 12:01:42 +00:00
|
|
|
addError("clients:clientScopeRemoveError", error);
|
2021-04-20 12:10:00 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{showAssign && (
|
|
|
|
<AddRoleMappingModal
|
|
|
|
id={id}
|
|
|
|
type={type}
|
|
|
|
name={name}
|
|
|
|
onAssign={assignRoles}
|
|
|
|
onClose={() => setShowAssign(false)}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<DeleteConfirm />
|
|
|
|
<KeycloakDataTable
|
|
|
|
data-testid="assigned-roles"
|
|
|
|
key={key}
|
|
|
|
loader={loader}
|
2021-06-15 06:14:38 +00:00
|
|
|
canSelectAll
|
|
|
|
onSelect={(rows) => setSelected(rows)}
|
2021-04-20 12:10:00 +00:00
|
|
|
searchPlaceholderKey="clients:searchByName"
|
|
|
|
ariaLabelKey="clients:clientScopeList"
|
2021-06-15 06:14:38 +00:00
|
|
|
isRowDisabled={(value) =>
|
|
|
|
(value.role as CompositeRole).isInherited || false
|
|
|
|
}
|
2021-04-20 12:10:00 +00:00
|
|
|
toolbarItem={
|
|
|
|
<>
|
|
|
|
<ToolbarItem>
|
|
|
|
<Checkbox
|
2022-04-05 15:02:27 +00:00
|
|
|
label={t("common:hideInheritedRoles")}
|
2021-04-20 12:10:00 +00:00
|
|
|
id="hideInheritedRoles"
|
|
|
|
isChecked={hide}
|
|
|
|
onChange={(check) => {
|
|
|
|
setHide(check);
|
|
|
|
onHideRolesToggle();
|
|
|
|
refresh();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</ToolbarItem>
|
|
|
|
<ToolbarItem>
|
|
|
|
<Button
|
|
|
|
data-testid="assignRole"
|
|
|
|
onClick={() => setShowAssign(true)}
|
|
|
|
>
|
2022-04-05 15:02:27 +00:00
|
|
|
{t("common:assignRole")}
|
2021-04-20 12:10:00 +00:00
|
|
|
</Button>
|
|
|
|
</ToolbarItem>
|
|
|
|
<ToolbarItem>
|
|
|
|
<Button
|
|
|
|
variant="link"
|
|
|
|
data-testid="unAssignRole"
|
|
|
|
onClick={toggleDeleteDialog}
|
|
|
|
isDisabled={selected.length === 0}
|
|
|
|
>
|
2022-04-05 15:02:27 +00:00
|
|
|
{t("common:unAssignRole")}
|
2021-04-20 12:10:00 +00:00
|
|
|
</Button>
|
|
|
|
</ToolbarItem>
|
|
|
|
</>
|
|
|
|
}
|
2021-06-22 05:52:13 +00:00
|
|
|
actions={[
|
|
|
|
{
|
2022-04-05 15:02:27 +00:00
|
|
|
title: t("common:unAssignRole"),
|
2021-06-22 05:52:13 +00:00
|
|
|
onRowClick: async (role) => {
|
|
|
|
setSelected([role]);
|
|
|
|
toggleDeleteDialog();
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]}
|
2021-04-20 12:10:00 +00:00
|
|
|
columns={[
|
|
|
|
{
|
|
|
|
name: "role.name",
|
2022-04-05 15:02:27 +00:00
|
|
|
displayKey: t("common:name"),
|
2022-04-29 07:03:39 +00:00
|
|
|
transforms: [cellWidth(30)],
|
2021-04-20 12:10:00 +00:00
|
|
|
cellRenderer: ServiceRole,
|
|
|
|
},
|
2022-05-02 05:51:09 +00:00
|
|
|
{
|
|
|
|
name: "role.isInherited",
|
|
|
|
displayKey: t("common:inherent"),
|
|
|
|
cellFormatters: [upperCaseFormatter(), emptyFormatter()],
|
|
|
|
},
|
2021-04-20 12:10:00 +00:00
|
|
|
{
|
|
|
|
name: "role.description",
|
2022-04-05 15:02:27 +00:00
|
|
|
displayKey: t("common:description"),
|
2021-04-20 12:10:00 +00:00
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
},
|
|
|
|
]}
|
2021-06-16 11:35:03 +00:00
|
|
|
emptyState={
|
|
|
|
<ListEmptyState
|
|
|
|
message={t("noRoles")}
|
|
|
|
instructions={t("noRolesInstructions")}
|
2022-04-05 15:02:27 +00:00
|
|
|
primaryActionText={t("common:assignRole")}
|
2021-06-16 11:35:03 +00:00
|
|
|
onPrimaryAction={() => setShowAssign(true)}
|
|
|
|
/>
|
|
|
|
}
|
2021-04-20 12:10:00 +00:00
|
|
|
/>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|