mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 21:18:18 +00:00
571 lines
21 KiB
TypeScript
571 lines
21 KiB
TypeScript
import {
|
|
faArrowDown,
|
|
faArrowUp,
|
|
faArrowUpRightFromSquare,
|
|
faCancel,
|
|
faCheck,
|
|
faCheckCircle,
|
|
faCopy,
|
|
faEdit,
|
|
faEllipsis,
|
|
faFileSignature,
|
|
faInfoCircle,
|
|
faKey,
|
|
faLock,
|
|
faLockOpen,
|
|
faMagnifyingGlass,
|
|
faPlus,
|
|
faTrash
|
|
} from "@fortawesome/free-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { motion } from "framer-motion";
|
|
|
|
import { createNotification } from "@app/components/notifications";
|
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
import {
|
|
Badge,
|
|
Button,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
EmptyState,
|
|
IconButton,
|
|
Input,
|
|
Pagination,
|
|
Spinner,
|
|
Table,
|
|
TableContainer,
|
|
TableSkeleton,
|
|
TBody,
|
|
Td,
|
|
Th,
|
|
THead,
|
|
Tooltip,
|
|
Tr
|
|
} from "@app/components/v2";
|
|
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
|
import {
|
|
ProjectPermissionActions,
|
|
ProjectPermissionCmekActions,
|
|
ProjectPermissionSub,
|
|
useProject,
|
|
useProjectPermission
|
|
} from "@app/context";
|
|
import { kmsKeyUsageOptions } from "@app/helpers/kms";
|
|
import {
|
|
getUserTablePreference,
|
|
PreferenceKey,
|
|
setUserTablePreference
|
|
} from "@app/helpers/userTablePreferences";
|
|
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
|
|
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
|
|
import { CmekOrderBy, KmsKeyUsage, TCmek } from "@app/hooks/api/cmeks/types";
|
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
|
|
|
import { CmekDecryptModal } from "./CmekDecryptModal";
|
|
import { CmekEncryptModal } from "./CmekEncryptModal";
|
|
import { CmekModal } from "./CmekModal";
|
|
import { CmekSignModal } from "./CmekSignModal";
|
|
import { CmekVerifyModal } from "./CmekVerifyModal";
|
|
import { DeleteCmekModal } from "./DeleteCmekModal";
|
|
|
|
const getStatusBadgeProps = (
|
|
isDisabled: boolean
|
|
): { variant: BadgeProps["variant"]; label: string } => {
|
|
if (isDisabled) {
|
|
return {
|
|
variant: "danger",
|
|
label: "Disabled"
|
|
};
|
|
}
|
|
|
|
return {
|
|
variant: "success",
|
|
label: "Active"
|
|
};
|
|
};
|
|
|
|
export const CmekTable = () => {
|
|
const { currentProject } = useProject();
|
|
const { permission } = useProjectPermission();
|
|
|
|
const projectId = currentProject?.id ?? "";
|
|
|
|
const {
|
|
offset,
|
|
limit,
|
|
orderBy,
|
|
orderDirection,
|
|
setOrderDirection,
|
|
search,
|
|
debouncedSearch,
|
|
setPage,
|
|
setSearch,
|
|
perPage,
|
|
page,
|
|
setPerPage
|
|
} = usePagination(CmekOrderBy.Name, {
|
|
initPerPage: getUserTablePreference("cmekClientTable", PreferenceKey.PerPage, 20)
|
|
});
|
|
|
|
const handlePerPageChange = (newPerPage: number) => {
|
|
setPerPage(newPerPage);
|
|
setUserTablePreference("cmekClientTable", PreferenceKey.PerPage, newPerPage);
|
|
};
|
|
|
|
const { data, isPending, isFetching } = useGetCmeksByProjectId({
|
|
projectId,
|
|
offset,
|
|
limit,
|
|
search: debouncedSearch,
|
|
orderBy,
|
|
orderDirection
|
|
});
|
|
|
|
const { keys = [], totalCount = 0 } = data ?? {};
|
|
useResetPageHelper({
|
|
totalCount,
|
|
offset,
|
|
setPage
|
|
});
|
|
|
|
const [, isCopyingCiphertext, setCopyCipherText] = useTimedReset<string>({
|
|
initialState: "",
|
|
delay: 1000
|
|
});
|
|
|
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
|
"upsertKey",
|
|
"deleteKey",
|
|
"encryptData",
|
|
"decryptData",
|
|
"signData",
|
|
"verifyData"
|
|
] as const);
|
|
|
|
const handleSort = () => {
|
|
setOrderDirection((prev) =>
|
|
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
|
);
|
|
};
|
|
|
|
const updateCmek = useUpdateCmek();
|
|
|
|
const handleDisableCmek = async ({ id: keyId, isDisabled }: TCmek) => {
|
|
try {
|
|
await updateCmek.mutateAsync({
|
|
keyId,
|
|
projectId,
|
|
isDisabled: !isDisabled
|
|
});
|
|
|
|
createNotification({
|
|
text: `Key successfully ${isDisabled ? "enabled" : "disabled"}`,
|
|
type: "success"
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
const error = err as any;
|
|
const text =
|
|
error?.response?.data?.message ?? `Failed to ${isDisabled ? "enable" : "disable"} key`;
|
|
|
|
createNotification({
|
|
text,
|
|
type: "error"
|
|
});
|
|
}
|
|
};
|
|
|
|
const cannotEditKey = permission.cannot(
|
|
ProjectPermissionCmekActions.Edit,
|
|
ProjectPermissionSub.Cmek
|
|
);
|
|
|
|
const cannotDeleteKey = permission.cannot(
|
|
ProjectPermissionCmekActions.Delete,
|
|
ProjectPermissionSub.Cmek
|
|
);
|
|
|
|
const cannotEncryptData = permission.cannot(
|
|
ProjectPermissionCmekActions.Encrypt,
|
|
ProjectPermissionSub.Cmek
|
|
);
|
|
|
|
const cannotDecryptData = permission.cannot(
|
|
ProjectPermissionCmekActions.Decrypt,
|
|
ProjectPermissionSub.Cmek
|
|
);
|
|
|
|
const cannotSignData = permission.cannot(
|
|
ProjectPermissionCmekActions.Sign,
|
|
ProjectPermissionSub.Cmek
|
|
);
|
|
|
|
const cannotVerifyData = permission.cannot(
|
|
ProjectPermissionCmekActions.Verify,
|
|
ProjectPermissionSub.Cmek
|
|
);
|
|
return (
|
|
<motion.div
|
|
key="kms-keys-tab"
|
|
transition={{ duration: 0.15 }}
|
|
initial={{ opacity: 0, translateX: 30 }}
|
|
animate={{ opacity: 1, translateX: 0 }}
|
|
exit={{ opacity: 0, translateX: 30 }}
|
|
>
|
|
<div className="border-mineshaft-600 bg-mineshaft-900 mb-6 rounded-lg border p-4">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<p className="text-mineshaft-100 whitespace-nowrap text-xl font-medium">Keys</p>
|
|
<div className="flex w-full justify-end pr-4">
|
|
<a
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href="https://infisical.com/docs/documentation/platform/kms"
|
|
>
|
|
<span className="border-mineshaft-500 bg-mineshaft-600 text-mineshaft-200 hover:border-primary/40 hover:bg-primary/10 flex w-max cursor-pointer items-center rounded-md border px-4 py-2 duration-200 hover:text-white">
|
|
Documentation{" "}
|
|
<FontAwesomeIcon
|
|
icon={faArrowUpRightFromSquare}
|
|
className="mb-[0.06rem] ml-1 text-xs"
|
|
/>
|
|
</span>
|
|
</a>
|
|
</div>
|
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Cmek}>
|
|
{(isAllowed) => (
|
|
<Button
|
|
colorSchema="primary"
|
|
type="submit"
|
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
onClick={() => handlePopUpOpen("upsertKey", null)}
|
|
isDisabled={!isAllowed}
|
|
>
|
|
Add Key
|
|
</Button>
|
|
)}
|
|
</ProjectPermissionCan>
|
|
</div>
|
|
<Input
|
|
containerClassName="mb-4"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
placeholder="Search keys by name..."
|
|
/>
|
|
<TableContainer>
|
|
<Table>
|
|
<THead>
|
|
<Tr className="h-14">
|
|
<Th>
|
|
<div className="flex items-center">
|
|
Name
|
|
<IconButton
|
|
variant="plain"
|
|
className="ml-2"
|
|
ariaLabel="sort"
|
|
onClick={handleSort}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
|
/>
|
|
</IconButton>
|
|
</div>
|
|
</Th>
|
|
<Th>Key ID</Th>
|
|
<Th>Key Usage</Th>
|
|
<Th>Algorithm</Th>
|
|
<Th>Status</Th>
|
|
<Th>Version</Th>
|
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
|
</Tr>
|
|
</THead>
|
|
<TBody>
|
|
{isPending && <TableSkeleton columns={5} innerKey="project-keys" />}
|
|
{!isPending &&
|
|
keys.length > 0 &&
|
|
keys.map((cmek) => {
|
|
const {
|
|
name,
|
|
id,
|
|
version,
|
|
description,
|
|
encryptionAlgorithm,
|
|
isDisabled,
|
|
keyUsage
|
|
} = cmek;
|
|
const { variant, label } = getStatusBadgeProps(isDisabled);
|
|
|
|
return (
|
|
<Tr
|
|
className="hover:bg-mineshaft-700 group h-10"
|
|
key={`st-v3-${id}`}
|
|
onMouseLeave={() => {
|
|
setCopyCipherText("");
|
|
}}
|
|
>
|
|
<Td>
|
|
<div className="flex items-center gap-2">
|
|
{name}
|
|
{description && (
|
|
<Tooltip content={description}>
|
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</Td>
|
|
<Td>
|
|
<div>
|
|
<span> {id}</span>
|
|
<IconButton
|
|
ariaLabel="copy icon"
|
|
colorSchema="secondary"
|
|
size="xs"
|
|
className="group/copy duration:0 invisible relative ml-3 rounded-md group-hover:visible"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(id);
|
|
setCopyCipherText("Copied");
|
|
}}
|
|
>
|
|
<FontAwesomeIcon icon={isCopyingCiphertext ? faCheck : faCopy} />
|
|
</IconButton>
|
|
</div>
|
|
</Td>
|
|
<Td>
|
|
<div className="flex items-center gap-2">
|
|
{kmsKeyUsageOptions[keyUsage].label}
|
|
<Tooltip content={kmsKeyUsageOptions[keyUsage].tooltip}>
|
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
|
</Tooltip>
|
|
</div>
|
|
</Td>
|
|
<Td className="uppercase">{encryptionAlgorithm}</Td>
|
|
<Td>
|
|
<Badge variant={variant}>{label}</Badge>
|
|
</Td>
|
|
<Td>{version}</Td>
|
|
<Td>
|
|
<div className="flex justify-end">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<IconButton
|
|
variant="plain"
|
|
colorSchema="primary"
|
|
className="data-[state=open]:text-primary-400 ml-4 p-0"
|
|
ariaLabel="More options"
|
|
>
|
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
|
</IconButton>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="min-w-[160px]">
|
|
{keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT && (
|
|
<>
|
|
<Tooltip
|
|
content={
|
|
// eslint-disable-next-line no-nested-ternary
|
|
cannotEncryptData
|
|
? "Access Restricted"
|
|
: isDisabled
|
|
? "Key Disabled"
|
|
: ""
|
|
}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
|
icon={<FontAwesomeIcon icon={faLock} />}
|
|
iconPos="left"
|
|
isDisabled={cannotEncryptData || isDisabled}
|
|
>
|
|
Encrypt Data
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip
|
|
content={
|
|
// eslint-disable-next-line no-nested-ternary
|
|
cannotDecryptData
|
|
? "Access Restricted"
|
|
: isDisabled
|
|
? "Key Disabled"
|
|
: ""
|
|
}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
|
icon={<FontAwesomeIcon icon={faLockOpen} />}
|
|
iconPos="left"
|
|
isDisabled={cannotDecryptData || isDisabled}
|
|
>
|
|
Decrypt Data
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
</>
|
|
)}
|
|
|
|
{keyUsage === KmsKeyUsage.SIGN_VERIFY && (
|
|
<>
|
|
<Tooltip
|
|
content={
|
|
// eslint-disable-next-line no-nested-ternary
|
|
cannotSignData
|
|
? "Access Restricted"
|
|
: isDisabled
|
|
? "Key Disabled"
|
|
: ""
|
|
}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handlePopUpOpen("signData", cmek)}
|
|
icon={<FontAwesomeIcon icon={faFileSignature} />}
|
|
iconPos="left"
|
|
isDisabled={cannotSignData || isDisabled}
|
|
>
|
|
Sign Data
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip
|
|
content={
|
|
// eslint-disable-next-line no-nested-ternary
|
|
cannotVerifyData
|
|
? "Access Restricted"
|
|
: isDisabled
|
|
? "Key Disabled"
|
|
: ""
|
|
}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handlePopUpOpen("verifyData", cmek)}
|
|
icon={<FontAwesomeIcon icon={faCheckCircle} />}
|
|
iconPos="left"
|
|
isDisabled={cannotVerifyData || isDisabled}
|
|
>
|
|
Verify Data
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
</>
|
|
)}
|
|
|
|
<Tooltip
|
|
content={cannotEditKey ? "Access Restricted" : ""}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handlePopUpOpen("upsertKey", cmek)}
|
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
|
iconPos="left"
|
|
isDisabled={cannotEditKey}
|
|
>
|
|
Edit Key
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip
|
|
content={cannotEditKey ? "Access Restricted" : ""}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handleDisableCmek(cmek)}
|
|
icon={
|
|
<FontAwesomeIcon
|
|
icon={isDisabled ? faCheckCircle : faCancel}
|
|
/>
|
|
}
|
|
iconPos="left"
|
|
isDisabled={cannotEditKey}
|
|
>
|
|
{isDisabled ? "Enable" : "Disable"} Key
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip
|
|
content={cannotDeleteKey ? "Access Restricted" : ""}
|
|
position="left"
|
|
>
|
|
<div>
|
|
<DropdownMenuItem
|
|
onClick={() => handlePopUpOpen("deleteKey", cmek)}
|
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
|
iconPos="left"
|
|
isDisabled={cannotDeleteKey}
|
|
>
|
|
Delete Key
|
|
</DropdownMenuItem>
|
|
</div>
|
|
</Tooltip>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</Td>
|
|
</Tr>
|
|
);
|
|
})}
|
|
</TBody>
|
|
</Table>
|
|
{!isPending && totalCount > 0 && (
|
|
<Pagination
|
|
count={totalCount}
|
|
page={page}
|
|
perPage={perPage}
|
|
onChangePage={(newPage) => setPage(newPage)}
|
|
onChangePerPage={handlePerPageChange}
|
|
/>
|
|
)}
|
|
{!isPending && keys.length === 0 && (
|
|
<EmptyState
|
|
title={
|
|
debouncedSearch.trim().length > 0
|
|
? "No keys match search filter"
|
|
: "No keys have been added to this project"
|
|
}
|
|
icon={faKey}
|
|
/>
|
|
)}
|
|
</TableContainer>
|
|
<DeleteCmekModal
|
|
isOpen={popUp.deleteKey.isOpen}
|
|
onOpenChange={(isOpen) => handlePopUpToggle("deleteKey", isOpen)}
|
|
cmek={popUp.deleteKey.data as TCmek}
|
|
/>
|
|
<CmekModal
|
|
isOpen={popUp.upsertKey.isOpen}
|
|
onOpenChange={(isOpen) => handlePopUpToggle("upsertKey", isOpen)}
|
|
cmek={popUp.upsertKey.data as TCmek | null}
|
|
/>
|
|
<CmekEncryptModal
|
|
isOpen={popUp.encryptData.isOpen}
|
|
onOpenChange={(isOpen) => handlePopUpToggle("encryptData", isOpen)}
|
|
cmek={popUp.encryptData.data as TCmek}
|
|
/>
|
|
<CmekDecryptModal
|
|
isOpen={popUp.decryptData.isOpen}
|
|
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
|
|
cmek={popUp.decryptData.data as TCmek}
|
|
/>
|
|
<CmekSignModal
|
|
isOpen={popUp.signData.isOpen}
|
|
onOpenChange={(isOpen) => handlePopUpToggle("signData", isOpen)}
|
|
cmek={popUp.signData.data as TCmek}
|
|
/>
|
|
<CmekVerifyModal
|
|
isOpen={popUp.verifyData.isOpen}
|
|
onOpenChange={(isOpen) => handlePopUpToggle("verifyData", isOpen)}
|
|
cmek={popUp.verifyData.data as TCmek}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
};
|