From 447630135b53fdfa7bdfb6ac391b0dfc09bca2ee Mon Sep 17 00:00:00 2001
From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com>
Date: Wed, 3 Apr 2024 16:50:32 -0700
Subject: [PATCH] Feat: Request access
---
.../AccessApprovalRequest.tsx | 577 ++++++++++++++++++
1 file changed, 577 insertions(+)
create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
new file mode 100644
index 000000000..ffb8ac049
--- /dev/null
+++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
@@ -0,0 +1,577 @@
+/* eslint-disable no-nested-ternary */
+/* eslint-disable react/jsx-no-useless-fragment */
+import { useCallback, useMemo, useState } from "react";
+import {
+ faCheck,
+ faCheckCircle,
+ faChevronDown,
+ faLockOpen
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { formatDistance } from "date-fns";
+import { AnimatePresence, motion } from "framer-motion";
+import ms from "ms";
+import { twMerge } from "tailwind-merge";
+
+import { createNotification } from "@app/components/notifications";
+import { ProjectPermissionCan } from "@app/components/permissions";
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+ EmptyState,
+ Modal,
+ ModalContent,
+ UpgradePlanModal
+} from "@app/components/v2";
+import {
+ ProjectPermissionActions,
+ ProjectPermissionSub,
+ useProjectPermission,
+ useSubscription,
+ useWorkspace
+} from "@app/context";
+import { usePopUp } from "@app/hooks";
+import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api";
+import {
+ useGetAccessApprovalPolicies,
+ useGetAccessApprovalRequests,
+ useGetAccessRequestsCount
+} from "@app/hooks/api/accessApproval/queries";
+import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
+import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types";
+import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection";
+
+const DisplayBadge = ({ text, className }: { text: string; className?: string }) => {
+ return (
+
+ {text}
+
+ );
+};
+
+const ReviewRequestModal = ({
+ isOpen,
+ onOpenChange,
+ request,
+ projectSlug,
+ selectedRequester,
+ selectedEnvSlug
+}: {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null };
+ projectSlug: string;
+ selectedRequester: string | undefined;
+ selectedEnvSlug: string | undefined;
+}) => {
+ const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
+
+ const accessDetails = {
+ env: request.environmentName,
+ // secret path will be inside $glob operator
+ secretPath: request.policy.secretPath,
+ read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)),
+ edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)),
+ create: request.permissions?.some(({ action }) =>
+ action.includes(ProjectPermissionActions.Create)
+ ),
+ delete: request.permissions?.some(({ action }) =>
+ action.includes(ProjectPermissionActions.Delete)
+ ),
+
+ temporaryAccess: {
+ isTemporary: request.isTemporary,
+ temporaryRange: request.temporaryRange
+ }
+ };
+
+ const requestedAccess = useMemo(() => {
+ const access: string[] = [];
+ if (accessDetails.read) access.push("Read");
+ if (accessDetails.edit) access.push("Edit");
+ if (accessDetails.create) access.push("Create");
+ if (accessDetails.delete) access.push("Delete");
+
+ return access.join(", ");
+ }, [accessDetails]);
+
+ const getAccessLabel = () => {
+ if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange)
+ return "Permanent";
+
+ // convert the range to human readable format
+ ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true });
+
+ return (
+
+ );
+ };
+
+ const reviewAccessRequest = useReviewAccessRequest();
+
+ const handleReview = useCallback(async (status: "approved" | "rejected") => {
+ setIsLoading(status);
+ try {
+ await reviewAccessRequest.mutateAsync({
+ requestId: request.id,
+ status,
+ projectSlug,
+ envSlug: selectedEnvSlug,
+ requestedBy: selectedRequester
+ });
+ } catch (error) {
+ console.error(error);
+ setIsLoading(null);
+ return;
+ }
+
+ createNotification({
+ title: `Request ${status}`,
+ text: `The request has been ${status}`,
+ type: status === "approved" ? "success" : "info"
+ });
+
+ setIsLoading(null);
+ onOpenChange(false);
+ }, []);
+
+ return (
+
+
+
+
+
+ {request.user?.firstName} {request.user?.lastName} ({request.user?.email})
+ {" "}
+ is requesting access to the following resource
+
+
+
+
+ Requested path:
+
+
+
+
+ Permissions:
+
+
+
+
+ Access Type:
+ {getAccessLabel()}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SelectAccessModal = ({
+ isOpen,
+ onOpenChange,
+ policies
+}: {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ policies: TAccessApprovalPolicy[];
+}) => {
+ return (
+
+
+ onOpenChange(false)} policies={policies} />
+
+
+ );
+};
+
+const generateRequestText = (request: TAccessApprovalRequest) => {
+ const { isTemporary } = request;
+
+ return (
+
+ Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
+
+ {request.policy.secretPath}
+
+ in
+
+ {request.environmentName}
+
+
+ );
+};
+
+export const AccessApprovalRequest = ({
+ projectSlug,
+ projectId
+}: {
+ projectSlug: string;
+ projectId: string;
+}) => {
+ const [selectedRequest, setSelectedRequest] = useState<
+ (TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }) | null
+ >(null);
+
+ const { handlePopUpOpen, popUp, handlePopUpClose } = usePopUp([
+ "requestAccess",
+ "reviewRequest",
+ "upgradePlan"
+ ] as const);
+ const { permission, membership } = useProjectPermission();
+ const { subscription } = useSubscription();
+ const { currentWorkspace } = useWorkspace();
+
+ const { data: members } = useGetWorkspaceUsers(projectId);
+ const membersGroupById = members?.reduce>(
+ (prev, curr) => ({ ...prev, [curr.id]: curr }),
+ {}
+ );
+
+ const [statusFilter, setStatusFilter] = useState<"open" | "close">("open");
+ const [requestedByFilter, setRequestedByFilter] = useState(undefined);
+ const [envFilter, setEnvFilter] = useState(undefined);
+
+ console.log("requestedByFilter", requestedByFilter);
+ console.log("envFilter", envFilter);
+
+ const { data: requestCount } = useGetAccessRequestsCount({
+ projectSlug
+ });
+
+ const { data: policies, isLoading: policiesLoading } = useGetAccessApprovalPolicies({
+ projectSlug,
+ options: {
+ enabled:
+ permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
+ !!projectSlug
+ }
+ });
+
+ const { data: requests } = useGetAccessApprovalRequests({
+ projectSlug,
+ authorProjectMembershipId: requestedByFilter,
+ envSlug: envFilter
+ });
+
+ const filteredRequests = useMemo(() => {
+ if (statusFilter === "open") return requests?.filter((request) => !request.isApproved);
+ if (statusFilter === "close") return requests?.filter((request) => request.isApproved);
+
+ return requests;
+ }, [requests, statusFilter, requestedByFilter, envFilter]);
+
+ const generateRequestDetails = (request: TAccessApprovalRequest) => {
+ const isReviewedByUser =
+ request.reviewers.findIndex(({ member }) => member === membership.id) !== -1;
+ const isRejectedByAnyone = request.reviewers.some(
+ ({ status }) => status === ApprovalStatus.REJECTED
+ );
+ const isApprover = request.policy.approvers.indexOf(membership.id || "") !== -1;
+ const isAccepted = request.isApproved;
+
+ const userReviewStatus = request.reviewers.find(
+ ({ member }) => member === membership.id
+ )?.status;
+
+ let displayData: { label: string; colorClass: string } = { label: "", colorClass: "" };
+
+ const isExpired =
+ request.privilege &&
+ request.isApproved &&
+ new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
+
+ if (isExpired) displayData = { label: "Access Expired", colorClass: "bg-red/20 text-red" };
+ else if (isAccepted)
+ displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" };
+ else if (isRejectedByAnyone)
+ displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" };
+ else if (userReviewStatus === ApprovalStatus.APPROVED)
+ displayData = {
+ label: `Pending ${request.policy.approvals - request.reviewers.length} reviews`,
+ colorClass: "bg-yellow/20 text-yellow"
+ };
+ else if (!isReviewedByUser)
+ displayData = {
+ label: "Review Required",
+ colorClass: "bg-yellow/20 text-yellow"
+ };
+
+ return {
+ displayData,
+ isReviewedByUser,
+ isRejectedByAnyone,
+ isApprover,
+ userReviewStatus,
+ isAccepted
+ };
+ };
+
+ return (
+
+
+
+
Access Approval Requests
+
+ Request access to secrets in sensitive environments and folders.
+
+
+
+
+ {(isAllowed) => (
+
+ )}
+
+
+
+
+
+
+
+
setStatusFilter("open")}
+ onKeyDown={(evt) => {
+ if (evt.key === "Enter") setStatusFilter("open");
+ }}
+ className={
+ statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
+ }
+ >
+
+ {!!requestCount && requestCount?.pendingCount} Pending
+
+
setStatusFilter("close")}
+ onKeyDown={(evt) => {
+ if (evt.key === "Enter") setStatusFilter("close");
+ }}
+ >
+
+ {!!requestCount && requestCount.finalizedCount} Completed
+
+
+
+
+ }
+ >
+ Environments
+
+
+
+ Select an environment
+ {currentWorkspace?.environments.map(({ slug, name }) => (
+ setEnvFilter((state) => (state === slug ? undefined : slug))}
+ key={`request-filter-${slug}`}
+ icon={envFilter === slug && }
+ iconPos="right"
+ >
+ {name}
+
+ ))}
+
+
+
+
+ }
+ >
+ Requested By
+
+
+
+ Select an author
+ {members?.map(({ user, id }) => (
+
+ setRequestedByFilter((state) => (state === id ? undefined : id))
+ }
+ key={`request-filter-member-${id}`}
+ icon={requestedByFilter === id && }
+ iconPos="right"
+ >
+ {user.email}
+
+ ))}
+
+
+
+
+
+ {filteredRequests?.length === 0 && (
+
+
+
+ )}
+ {!!filteredRequests?.length &&
+ requests?.map((request) => {
+ const details = generateRequestDetails(request);
+
+ return (
+
{
+ if (details.isReviewedByUser || details.isRejectedByAnyone) return;
+
+ setSelectedRequest({
+ ...request,
+ user: membersGroupById?.[request.requestedBy].user!
+ });
+ handlePopUpOpen("reviewRequest");
+ }}
+ onKeyDown={(evt) => {
+ if (evt.key === "Enter") {
+ setSelectedRequest({
+ ...request,
+ user: membersGroupById?.[request.requestedBy].user!
+ });
+ handlePopUpOpen("reviewRequest");
+ }
+ }}
+ >
+
+
+
+ {generateRequestText(request)}
+
+
+ Requested {formatDistance(new Date(request.createdAt), new Date())} ago by{" "}
+ {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "}
+ {membersGroupById?.[request.requestedBy]?.user?.lastName} (
+ {membersGroupById?.[request.requestedBy]?.user?.email}){" "}
+
+ {details.isApprover &&
+ !details.isReviewedByUser &&
+ !details.isAccepted &&
+ "- Review required"}
+
+
+
+ {details.isApprover && (
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ {!!policies && (
+
handlePopUpClose("requestAccess")}
+ />
+ )}
+
+ {!!selectedRequest && (
+ {
+ handlePopUpClose("reviewRequest");
+ setSelectedRequest(null);
+ }}
+ />
+ )}
+
+ handlePopUpClose("upgradePlan")}
+ />
+
+ );
+};