mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 00:29:04 +00:00
448 lines
18 KiB
TypeScript
448 lines
18 KiB
TypeScript
import Link from "next/link";
|
|
import {
|
|
faArrowDown,
|
|
faArrowUp,
|
|
faArrowUpRightFromSquare,
|
|
faClock,
|
|
faEdit,
|
|
faMagnifyingGlass,
|
|
faPlus,
|
|
faServer,
|
|
faXmark
|
|
} from "@fortawesome/free-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { format } from "date-fns";
|
|
import { motion } from "framer-motion";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
import { createNotification } from "@app/components/notifications";
|
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
import {
|
|
Button,
|
|
DeleteActionModal,
|
|
EmptyState,
|
|
HoverCard,
|
|
HoverCardContent,
|
|
HoverCardTrigger,
|
|
IconButton,
|
|
Input,
|
|
Modal,
|
|
ModalContent,
|
|
Pagination,
|
|
Spinner,
|
|
Table,
|
|
TableContainer,
|
|
TableSkeleton,
|
|
Tag,
|
|
TBody,
|
|
Td,
|
|
Th,
|
|
THead,
|
|
Tooltip,
|
|
Tr
|
|
} from "@app/components/v2";
|
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
import { withProjectPermission } from "@app/hoc";
|
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
|
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
|
|
import { usePopUp } from "@app/hooks/usePopUp";
|
|
|
|
import { IdentityModal } from "./components/IdentityModal";
|
|
import { IdentityRoleForm } from "./components/IdentityRoleForm";
|
|
|
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
|
|
|
const formatRoleName = (role: string, customRoleName?: string) => {
|
|
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
|
if (role === ProjectMembershipRole.Member) return "Developer";
|
|
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
|
return role;
|
|
};
|
|
export const IdentityTab = withProjectPermission(
|
|
() => {
|
|
const { currentWorkspace } = useWorkspace();
|
|
|
|
const {
|
|
offset,
|
|
limit,
|
|
orderBy,
|
|
setOrderBy,
|
|
orderDirection,
|
|
setOrderDirection,
|
|
search,
|
|
debouncedSearch,
|
|
setPage,
|
|
setSearch,
|
|
perPage,
|
|
page,
|
|
setPerPage
|
|
} = usePagination(ProjectIdentityOrderBy.Name);
|
|
|
|
const workspaceId = currentWorkspace?.id ?? "";
|
|
|
|
const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships(
|
|
{
|
|
workspaceId: currentWorkspace?.id || "",
|
|
offset,
|
|
limit,
|
|
orderDirection,
|
|
orderBy,
|
|
search: debouncedSearch
|
|
},
|
|
{ keepPreviousData: true }
|
|
);
|
|
|
|
const { totalCount = 0 } = data ?? {};
|
|
|
|
useResetPageHelper({
|
|
totalCount,
|
|
offset,
|
|
setPage
|
|
});
|
|
|
|
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
|
|
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
|
"identity",
|
|
"deleteIdentity",
|
|
"upgradePlan",
|
|
"updateRole"
|
|
] as const);
|
|
|
|
const onRemoveIdentitySubmit = async (identityId: string) => {
|
|
try {
|
|
await deleteMutateAsync({
|
|
identityId,
|
|
workspaceId
|
|
});
|
|
|
|
createNotification({
|
|
text: "Successfully removed identity from project",
|
|
type: "success"
|
|
});
|
|
|
|
handlePopUpClose("deleteIdentity");
|
|
} catch (err) {
|
|
console.error(err);
|
|
const error = err as any;
|
|
const text = error?.response?.data?.message ?? "Failed to remove identity from project";
|
|
|
|
createNotification({
|
|
text,
|
|
type: "error"
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSort = (column: ProjectIdentityOrderBy) => {
|
|
if (column === orderBy) {
|
|
setOrderDirection((prev) =>
|
|
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
|
);
|
|
return;
|
|
}
|
|
|
|
setOrderBy(column);
|
|
setOrderDirection(OrderByDirection.ASC);
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
key="identity-role-panel"
|
|
transition={{ duration: 0.15 }}
|
|
initial={{ opacity: 0, translateX: 30 }}
|
|
animate={{ opacity: 1, translateX: 0 }}
|
|
exit={{ opacity: 0, translateX: 30 }}
|
|
>
|
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
|
<div className="flex w-full justify-end pr-4">
|
|
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
|
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
|
Documentation{" "}
|
|
<FontAwesomeIcon
|
|
icon={faArrowUpRightFromSquare}
|
|
className="mb-[0.06rem] ml-1 text-xs"
|
|
/>
|
|
</span>
|
|
</Link>
|
|
</div>
|
|
<ProjectPermissionCan
|
|
I={ProjectPermissionActions.Create}
|
|
a={ProjectPermissionSub.Identity}
|
|
>
|
|
{(isAllowed) => (
|
|
<Button
|
|
colorSchema="primary"
|
|
type="submit"
|
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
onClick={() => handlePopUpOpen("identity")}
|
|
isDisabled={!isAllowed}
|
|
>
|
|
Add identity
|
|
</Button>
|
|
)}
|
|
</ProjectPermissionCan>
|
|
</div>
|
|
<Input
|
|
containerClassName="mb-4"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
placeholder="Search identities by name..."
|
|
/>
|
|
<TableContainer>
|
|
<Table>
|
|
<THead>
|
|
<Tr className="h-14">
|
|
<Th className="w-1/3">
|
|
<div className="flex items-center">
|
|
Name
|
|
<IconButton
|
|
variant="plain"
|
|
className={`ml-2 ${
|
|
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
|
|
}`}
|
|
ariaLabel="sort"
|
|
onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
orderDirection === OrderByDirection.DESC &&
|
|
orderBy === ProjectIdentityOrderBy.Name
|
|
? faArrowUp
|
|
: faArrowDown
|
|
}
|
|
/>
|
|
</IconButton>
|
|
</div>
|
|
</Th>
|
|
<Th className="w-1/3">Role</Th>
|
|
<Th>Added on</Th>
|
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
|
</Tr>
|
|
</THead>
|
|
<TBody>
|
|
{isLoading && <TableSkeleton columns={4} innerKey="project-identities" />}
|
|
{!isLoading &&
|
|
data &&
|
|
data.identityMemberships.length > 0 &&
|
|
data.identityMemberships.map((identityMember, index) => {
|
|
const {
|
|
identity: { id, name },
|
|
roles,
|
|
createdAt
|
|
} = identityMember;
|
|
return (
|
|
<Tr className="h-10" key={`st-v3-${id}`}>
|
|
<Td>{name}</Td>
|
|
|
|
<Td>
|
|
<div className="flex items-center space-x-2">
|
|
{roles
|
|
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
|
.map(
|
|
({
|
|
role,
|
|
customRoleName,
|
|
id: roleId,
|
|
isTemporary,
|
|
temporaryAccessEndTime
|
|
}) => {
|
|
const isExpired =
|
|
new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
|
return (
|
|
<Tag key={roleId}>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="capitalize">
|
|
{formatRoleName(role, customRoleName)}
|
|
</div>
|
|
{isTemporary && (
|
|
<div>
|
|
<Tooltip
|
|
content={
|
|
isExpired
|
|
? "Timed role expired"
|
|
: "Timed role access"
|
|
}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={faClock}
|
|
className={twMerge(isExpired && "text-red-600")}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Tag>
|
|
);
|
|
}
|
|
)}
|
|
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
|
|
<HoverCard>
|
|
<HoverCardTrigger>
|
|
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
|
|
{roles
|
|
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
|
.map(
|
|
({
|
|
role,
|
|
customRoleName,
|
|
id: roleId,
|
|
isTemporary,
|
|
temporaryAccessEndTime
|
|
}) => {
|
|
const isExpired =
|
|
new Date() >
|
|
new Date(temporaryAccessEndTime || ("" as string));
|
|
return (
|
|
<Tag key={roleId} className="capitalize">
|
|
<div className="flex items-center space-x-2">
|
|
<div>{formatRoleName(role, customRoleName)}</div>
|
|
{isTemporary && (
|
|
<div>
|
|
<Tooltip
|
|
content={
|
|
isExpired
|
|
? "Access expired"
|
|
: "Temporary access"
|
|
}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={faClock}
|
|
className={twMerge(
|
|
new Date() >
|
|
new Date(
|
|
temporaryAccessEndTime as string
|
|
) && "text-red-600"
|
|
)}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Tag>
|
|
);
|
|
}
|
|
)}
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
)}
|
|
<Tooltip content="Edit permission">
|
|
<IconButton
|
|
size="sm"
|
|
variant="plain"
|
|
ariaLabel="update-role"
|
|
onClick={() =>
|
|
handlePopUpOpen("updateRole", { ...identityMember, index })
|
|
}
|
|
>
|
|
<FontAwesomeIcon icon={faEdit} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
</Td>
|
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
|
<Td className="flex justify-end">
|
|
<ProjectPermissionCan
|
|
I={ProjectPermissionActions.Delete}
|
|
a={ProjectPermissionSub.Identity}
|
|
>
|
|
{(isAllowed) => (
|
|
<IconButton
|
|
onClick={() => {
|
|
handlePopUpOpen("deleteIdentity", {
|
|
identityId: id,
|
|
name
|
|
});
|
|
}}
|
|
size="lg"
|
|
colorSchema="danger"
|
|
variant="plain"
|
|
ariaLabel="update"
|
|
className="ml-4"
|
|
isDisabled={!isAllowed}
|
|
>
|
|
<FontAwesomeIcon icon={faXmark} />
|
|
</IconButton>
|
|
)}
|
|
</ProjectPermissionCan>
|
|
</Td>
|
|
</Tr>
|
|
);
|
|
})}
|
|
</TBody>
|
|
</Table>
|
|
{!isLoading && data && totalCount > 0 && (
|
|
<Pagination
|
|
count={totalCount}
|
|
page={page}
|
|
perPage={perPage}
|
|
onChangePage={(newPage) => setPage(newPage)}
|
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
|
/>
|
|
)}
|
|
{!isLoading && data && data?.identityMemberships.length === 0 && (
|
|
<EmptyState
|
|
title={
|
|
debouncedSearch.trim().length > 0
|
|
? "No identities match search filter"
|
|
: "No identities have been added to this project"
|
|
}
|
|
icon={faServer}
|
|
/>
|
|
)}
|
|
</TableContainer>
|
|
<Modal
|
|
isOpen={popUp.updateRole.isOpen}
|
|
onOpenChange={(state) => handlePopUpToggle("updateRole", state)}
|
|
>
|
|
<ModalContent
|
|
className="max-w-3xl"
|
|
title={`Manage Access for ${
|
|
(popUp.updateRole.data as IdentityMembership)?.identity?.name
|
|
}`}
|
|
subTitle={`
|
|
Configure role-based access control by assigning machine identities a mix of roles and specific privileges. An identity will gain access to all actions within the roles assigned to it, not just the actions those roles share in common. You must choose at least one permanent role.
|
|
`}
|
|
>
|
|
<IdentityRoleForm
|
|
onOpenUpgradeModal={(description) =>
|
|
handlePopUpOpen("upgradePlan", { description })
|
|
}
|
|
identityProjectMember={
|
|
data?.identityMemberships[
|
|
(popUp.updateRole?.data as IdentityMembership & { index: number })?.index
|
|
] as IdentityMembership
|
|
}
|
|
/>
|
|
</ModalContent>
|
|
</Modal>
|
|
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
|
<DeleteActionModal
|
|
isOpen={popUp.deleteIdentity.isOpen}
|
|
title={`Are you sure want to remove ${
|
|
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
|
} from the project?`}
|
|
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
|
deleteKey="confirm"
|
|
onDeleteApproved={() =>
|
|
onRemoveIdentitySubmit(
|
|
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
},
|
|
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
|
|
);
|