371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
import { useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { useNavigate } from "react-router-dom-v5-compat";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
AlertVariant,
|
|
ButtonVariant,
|
|
DescriptionList,
|
|
Dropdown,
|
|
DropdownItem,
|
|
DropdownToggle,
|
|
PageSection,
|
|
ToolbarItem,
|
|
} from "@patternfly/react-core";
|
|
import {
|
|
ExpandableRowContent,
|
|
TableComposable,
|
|
Tbody,
|
|
Td,
|
|
Th,
|
|
Thead,
|
|
Tr,
|
|
} from "@patternfly/react-table";
|
|
|
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
|
import type PolicyProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyProviderRepresentation";
|
|
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
|
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
|
import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar";
|
|
import { useAlerts } from "../../components/alert/Alerts";
|
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
|
import useToggle from "../../utils/useToggle";
|
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
|
import { SearchDropdown, SearchForm } from "./SearchDropdown";
|
|
import { MoreLabel } from "./MoreLabel";
|
|
import { DetailDescriptionLink } from "./DetailDescription";
|
|
import { EmptyPermissionsState } from "./EmptyPermissionsState";
|
|
import { toNewPermission } from "../routes/NewPermission";
|
|
import { toPermissionDetails } from "../routes/PermissionDetails";
|
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
|
import { toPolicyDetails } from "../routes/PolicyDetails";
|
|
|
|
import "./permissions.css";
|
|
|
|
type PermissionsProps = {
|
|
clientId: string;
|
|
};
|
|
|
|
type ExpandablePolicyRepresentation = PolicyRepresentation & {
|
|
associatedPolicies?: PolicyRepresentation[];
|
|
isExpanded: boolean;
|
|
};
|
|
|
|
export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
|
const { t } = useTranslation("clients");
|
|
const navigate = useNavigate();
|
|
const { adminClient } = useAdminClient();
|
|
const { addAlert, addError } = useAlerts();
|
|
const { realm } = useRealm();
|
|
|
|
const [permissions, setPermissions] =
|
|
useState<ExpandablePolicyRepresentation[]>();
|
|
const [selectedPermission, setSelectedPermission] =
|
|
useState<PolicyRepresentation>();
|
|
const [policyProviders, setPolicyProviders] =
|
|
useState<PolicyProviderRepresentation[]>();
|
|
const [disabledCreate, setDisabledCreate] = useState<{
|
|
resources: boolean;
|
|
scopes: boolean;
|
|
}>();
|
|
const [createOpen, toggleCreate] = useToggle();
|
|
const [search, setSearch] = useState<SearchForm>({});
|
|
|
|
const [key, setKey] = useState(0);
|
|
const refresh = () => setKey(key + 1);
|
|
|
|
const [max, setMax] = useState(10);
|
|
const [first, setFirst] = useState(0);
|
|
|
|
const AssociatedPoliciesRenderer = ({
|
|
row,
|
|
}: {
|
|
row: ExpandablePolicyRepresentation;
|
|
}) => {
|
|
return (
|
|
<>
|
|
{row.associatedPolicies?.[0]?.name}{" "}
|
|
<MoreLabel array={row.associatedPolicies} />
|
|
</>
|
|
);
|
|
};
|
|
|
|
useFetch(
|
|
async () => {
|
|
const permissions = await adminClient.clients.findPermissions({
|
|
first,
|
|
max: max + 1,
|
|
id: clientId,
|
|
...search,
|
|
});
|
|
|
|
return await Promise.all(
|
|
permissions.map(async (permission) => {
|
|
const associatedPolicies =
|
|
await adminClient.clients.getAssociatedPolicies({
|
|
id: clientId,
|
|
permissionId: permission.id!,
|
|
});
|
|
|
|
return {
|
|
...permission,
|
|
associatedPolicies,
|
|
isExpanded: false,
|
|
};
|
|
})
|
|
);
|
|
},
|
|
setPermissions,
|
|
[key, search, first, max]
|
|
);
|
|
|
|
useFetch(
|
|
async () => {
|
|
const params = {
|
|
first: 0,
|
|
max: 1,
|
|
};
|
|
const [policies, resources, scopes] = await Promise.all([
|
|
adminClient.clients.listPolicyProviders({
|
|
id: clientId,
|
|
}),
|
|
adminClient.clients.listResources({ ...params, id: clientId }),
|
|
adminClient.clients.listAllScopes({ ...params, id: clientId }),
|
|
]);
|
|
return {
|
|
policies: policies.filter(
|
|
(p) => p.type === "resource" || p.type === "scope"
|
|
),
|
|
resources: resources.length !== 1,
|
|
scopes: scopes.length !== 1,
|
|
};
|
|
},
|
|
({ policies, resources, scopes }) => {
|
|
setPolicyProviders(policies);
|
|
setDisabledCreate({ resources, scopes });
|
|
},
|
|
[]
|
|
);
|
|
|
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
|
titleKey: "clients:deletePermission",
|
|
messageKey: t("deletePermissionConfirm", {
|
|
permission: selectedPermission?.name,
|
|
}),
|
|
continueButtonVariant: ButtonVariant.danger,
|
|
continueButtonLabel: "clients:confirm",
|
|
onConfirm: async () => {
|
|
try {
|
|
await adminClient.clients.delPermission({
|
|
id: clientId,
|
|
type: selectedPermission?.type!,
|
|
permissionId: selectedPermission?.id!,
|
|
});
|
|
addAlert(t("permissionDeletedSuccess"), AlertVariant.success);
|
|
refresh();
|
|
} catch (error) {
|
|
addError("clients:permissionDeletedError", error);
|
|
}
|
|
},
|
|
});
|
|
|
|
if (!permissions) {
|
|
return <KeycloakSpinner />;
|
|
}
|
|
|
|
const noData = permissions.length === 0;
|
|
const searching = Object.keys(search).length !== 0;
|
|
return (
|
|
<PageSection variant="light" className="pf-u-p-0">
|
|
<DeleteConfirm />
|
|
{(!noData || searching) && (
|
|
<PaginatingTableToolbar
|
|
count={permissions.length}
|
|
first={first}
|
|
max={max}
|
|
onNextClick={setFirst}
|
|
onPreviousClick={setFirst}
|
|
onPerPageSelect={(first, max) => {
|
|
setFirst(first);
|
|
setMax(max);
|
|
}}
|
|
toolbarItem={
|
|
<>
|
|
<ToolbarItem>
|
|
<SearchDropdown
|
|
types={policyProviders}
|
|
search={search}
|
|
onSearch={setSearch}
|
|
/>
|
|
</ToolbarItem>
|
|
<ToolbarItem>
|
|
<Dropdown
|
|
toggle={
|
|
<DropdownToggle
|
|
onToggle={toggleCreate}
|
|
isPrimary
|
|
data-testid="permissionCreateDropdown"
|
|
>
|
|
{t("createPermission")}
|
|
</DropdownToggle>
|
|
}
|
|
isOpen={createOpen}
|
|
dropdownItems={[
|
|
<DropdownItem
|
|
data-testid="create-resource"
|
|
key="createResourceBasedPermission"
|
|
isDisabled={disabledCreate?.resources}
|
|
component="button"
|
|
onClick={() =>
|
|
navigate(
|
|
toNewPermission({
|
|
realm,
|
|
id: clientId,
|
|
permissionType: "resource",
|
|
})
|
|
)
|
|
}
|
|
>
|
|
{t("createResourceBasedPermission")}
|
|
</DropdownItem>,
|
|
<DropdownItem
|
|
data-testid="create-scope"
|
|
key="createScopeBasedPermission"
|
|
isDisabled={disabledCreate?.scopes}
|
|
component="button"
|
|
onClick={() =>
|
|
navigate(
|
|
toNewPermission({
|
|
realm,
|
|
id: clientId,
|
|
permissionType: "scope",
|
|
})
|
|
)
|
|
}
|
|
>
|
|
{t("createScopeBasedPermission")}
|
|
</DropdownItem>,
|
|
]}
|
|
/>
|
|
</ToolbarItem>
|
|
</>
|
|
}
|
|
>
|
|
{!noData && (
|
|
<TableComposable aria-label={t("resources")} variant="compact">
|
|
<Thead>
|
|
<Tr>
|
|
<Th />
|
|
<Th>{t("common:name")}</Th>
|
|
<Th>{t("common:type")}</Th>
|
|
<Th>{t("associatedPolicy")}</Th>
|
|
<Th>{t("common:description")}</Th>
|
|
<Th />
|
|
</Tr>
|
|
</Thead>
|
|
{permissions.map((permission, rowIndex) => (
|
|
<Tbody key={permission.id} isExpanded={permission.isExpanded}>
|
|
<Tr>
|
|
<Td
|
|
expand={{
|
|
rowIndex,
|
|
isExpanded: permission.isExpanded,
|
|
onToggle: (_, rowIndex) => {
|
|
const rows = permissions.map((p, index) =>
|
|
index === rowIndex
|
|
? { ...p, isExpanded: !p.isExpanded }
|
|
: p
|
|
);
|
|
setPermissions(rows);
|
|
},
|
|
}}
|
|
/>
|
|
<Td data-testid={`name-column-${permission.name}`}>
|
|
<Link
|
|
to={toPermissionDetails({
|
|
realm,
|
|
id: clientId,
|
|
permissionType: permission.type!,
|
|
permissionId: permission.id!,
|
|
})}
|
|
>
|
|
{permission.name}
|
|
</Link>
|
|
</Td>
|
|
<Td>
|
|
{
|
|
policyProviders?.find((p) => p.type === permission.type)
|
|
?.name
|
|
}
|
|
</Td>
|
|
<Td>
|
|
<AssociatedPoliciesRenderer row={permission} />
|
|
</Td>
|
|
<Td>{permission.description}</Td>
|
|
<Td
|
|
actions={{
|
|
items: [
|
|
{
|
|
title: t("common:delete"),
|
|
onClick: async () => {
|
|
setSelectedPermission(permission);
|
|
toggleDeleteDialog();
|
|
},
|
|
},
|
|
],
|
|
}}
|
|
></Td>
|
|
</Tr>
|
|
<Tr
|
|
key={`child-${permission.id}`}
|
|
isExpanded={permission.isExpanded}
|
|
>
|
|
<Td />
|
|
<Td colSpan={5}>
|
|
<ExpandableRowContent>
|
|
{permission.isExpanded && (
|
|
<DescriptionList
|
|
isHorizontal
|
|
className="keycloak_resource_details"
|
|
>
|
|
<DetailDescriptionLink
|
|
name="associatedPolicy"
|
|
array={permission.associatedPolicies}
|
|
convert={(p) => p.name!}
|
|
link={(p) =>
|
|
toPolicyDetails({
|
|
id: clientId,
|
|
realm,
|
|
policyId: p.id!,
|
|
policyType: p.type!,
|
|
})
|
|
}
|
|
/>
|
|
</DescriptionList>
|
|
)}
|
|
</ExpandableRowContent>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
))}
|
|
</TableComposable>
|
|
)}
|
|
</PaginatingTableToolbar>
|
|
)}
|
|
{noData && !searching && (
|
|
<EmptyPermissionsState
|
|
clientId={clientId}
|
|
isResourceEnabled={disabledCreate?.resources}
|
|
isScopeEnabled={disabledCreate?.scopes}
|
|
/>
|
|
)}
|
|
{noData && searching && (
|
|
<ListEmptyState
|
|
isSearchVariant
|
|
message={t("common:noSearchResults")}
|
|
instructions={t("common:noSearchResultsInstructions")}
|
|
/>
|
|
)}
|
|
</PageSection>
|
|
);
|
|
};
|