From 7aee4fdfcdfdf3f312504e6e62841b6cc07a97e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:45:37 -0700 Subject: [PATCH] Feat: Request access --- .../src/hooks/api/accessApproval/mutation.tsx | 87 ++++++++-- .../src/hooks/api/accessApproval/queries.tsx | 149 ++++++++++++++++-- .../src/hooks/api/accessApproval/types.ts | 106 ++++++++++++- 3 files changed, 312 insertions(+), 30 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 98e9b3f97..7a150092c 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -1,26 +1,34 @@ +import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { accessApprovalKeys } from "./queries"; -import { TCreateAccessPolicyDTO, TDeleteSecretPolicyDTO, TUpdateAccessPolicyDTO } from "./types"; +import { + TAccessApproval, + TCreateAccessPolicyDTO, + TCreateAccessRequestDTO, + TDeleteSecretPolicyDTO, + TUpdateAccessPolicyDTO +} from "./types"; export const useCreateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, name }) => { + mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { const { data } = await apiRequest.post("/api/v1/access-approvals", { environment, - workspaceId, + projectSlug, approvals, approvers, + secretPath, name }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); } }); }; @@ -29,16 +37,17 @@ export const useUpdateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, name }) => { + mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { approvals, approvers, + secretPath, name }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); } }); }; @@ -51,8 +60,66 @@ export const useDeleteAccessApprovalPolicy = () => { const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); + } + }); +}; + +export const useCreateAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TCreateAccessRequestDTO>({ + mutationFn: async ({ envSlug, projectSlug, secretPath, ...privilege }) => { + const { data } = await apiRequest.post( + "/api/v1/access-approval-requests", + { + ...privilege, + permissions: privilege.permissions ? packRules(privilege.permissions) : undefined + }, + { + params: { + envSlug, + projectSlug, + secretPath + } + } + ); + + return data; + }, + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequests(projectSlug)); + } + }); +}; + +export const useReviewAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation< + {}, + {}, + { + requestId: string; + status: "approved" | "rejected"; + projectSlug: string; + envSlug?: string; + requestedBy?: string; + } + >({ + mutationFn: async ({ requestId, status }) => { + const { data } = await apiRequest.post( + `/api/v1/access-approval-requests/${requestId}/review`, + { + status + } + ); + return data; + }, + onSuccess: (_, { projectSlug, envSlug, requestedBy }) => { + queryClient.invalidateQueries( + accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy) + ); + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)); } }); }; diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 864122593..112c53fe3 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -1,30 +1,125 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TAccessApprovalPolicy, TGetSecretApprovalPoliciesDTO } from "./types"; +import { TProjectPermission } from "../roles/types"; +import { + TAccessApprovalPolicy, + TAccessApprovalRequest, + TAccessRequestCount, + TGetAccessApprovalRequestsDTO, + TGetAccessPolicyApprovalCountDTO +} from "./types"; export const accessApprovalKeys = { - getAccessApprovalPolicies: (workspaceId: string) => - [{ workspaceId }, "access-approval-policies"] as const, - getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => [ - { workspaceId, environment }, - "access-approval-policy" - ] + getAccessApprovalPolicies: (projectSlug: string) => + [{ projectSlug }, "access-approval-policies"] as const, + getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => + [{ workspaceId, environment }, "access-approval-policy"] as const, + + getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) => + [{ projectSlug, envSlug, requestedBy }, "access-approval-requests"] as const, + getAccessApprovalRequestCount: (projectSlug: string) => + [{ projectSlug }, "access-approval-request-count"] as const }; -const fetchApprovalPolicies = async (workspaceId: string) => { +export const fetchPolicyApprovalCount = async ({ + projectSlug, + envSlug +}: TGetAccessPolicyApprovalCountDTO) => { + const { data } = await apiRequest.get<{ policyCount: number }>( + "/api/v1/access-approvals/policy-count", + { + params: { projectSlug, envSlug } + } + ); + return data.policyCount; +}; + +export const useGetAccessPolicyApprovalCount = ({ + projectSlug, + envSlug, + options = {} +}: TGetAccessPolicyApprovalCountDTO & { + options?: UseQueryOptions< + number, + unknown, + number, + ReturnType + >; +}) => + useQuery({ + queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( "/api/v1/access-approvals", - { params: { workspaceId } } + { params: { projectSlug } } ); return data.approvals; }; -export const useGetAccessApprovalPolicies = ({ - workspaceId, +const fetchApprovalRequests = async ({ + projectSlug, + envSlug, + authorProjectMembershipId +}: TGetAccessApprovalRequestsDTO) => { + const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( + "/api/v1/access-approval-requests", + { params: { projectSlug, envSlug, authorProjectMembershipId } } + ); + + return data.requests.map((request) => ({ + ...request, + + privilege: request.privilege + ? { + ...request.privilege, + permissions: unpackRules( + request.privilege.permissions as unknown as PackRule[] + ) + } + : null, + permissions: unpackRules(request.permissions as unknown as PackRule[]) + })); +}; + +const fetchAccessRequestsCount = async (projectSlug: string) => { + const { data } = await apiRequest.get( + "/api/v1/access-approval-requests/count", + { params: { projectSlug } } + ); + return data; +}; + +export const useGetAccessRequestsCount = ({ + projectSlug, options = {} -}: TGetSecretApprovalPoliciesDTO & { +}: TGetAccessApprovalRequestsDTO & { + options?: UseQueryOptions< + TAccessRequestCount, + unknown, + { pendingCount: number; finalizedCount: number }, + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug), + queryFn: () => fetchAccessRequestsCount(projectSlug), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +export const useGetAccessApprovalPolicies = ({ + projectSlug, + envSlug, + authorProjectMembershipId, + options = {} +}: TGetAccessApprovalRequestsDTO & { options?: UseQueryOptions< TAccessApprovalPolicy[], unknown, @@ -33,8 +128,32 @@ export const useGetAccessApprovalPolicies = ({ >; }) => useQuery({ - queryKey: accessApprovalKeys.getAccessApprovalPolicies(workspaceId), - queryFn: () => fetchApprovalPolicies(workspaceId), + queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug), + queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }), ...options, - enabled: Boolean(workspaceId) && (options?.enabled ?? true) + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +export const useGetAccessApprovalRequests = ({ + projectSlug, + envSlug, + authorProjectMembershipId, + options = {} +}: TGetAccessApprovalRequestsDTO & { + options?: UseQueryOptions< + TAccessApprovalRequest[], + unknown, + TAccessApprovalRequest[], + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalRequests( + projectSlug, + envSlug, + authorProjectMembershipId + ), + queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) }); diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 63020166b..1ffd0bf2e 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -1,9 +1,11 @@ +import { TProjectPermission } from "../roles/types"; import { WorkspaceEnv } from "../workspace/types"; export type TAccessApprovalPolicy = { id: string; name: string; approvals: number; + secretPath: string; envId: string; workspace: string; environment: WorkspaceEnv; @@ -11,8 +13,99 @@ export type TAccessApprovalPolicy = { approvers: string[]; }; -export type TGetSecretApprovalPoliciesDTO = { - workspaceId: string; +export type TAccessApprovalRequest = { + id: string; + policyId: string; + privilegeId: string | null; + requestedBy: string; + createdAt: Date; + updatedAt: Date; + isTemporary: boolean; + temporaryRange: string | null | undefined; + + permissions: TProjectPermission[] | null; + + // Computed + environmentName: string; + isApproved: boolean; + + privilege: { + membershipId: string; + isTemporary: boolean; + temporaryMode?: string | null; + temporaryRange?: string | null; + temporaryAccessStartTime?: Date | null; + temporaryAccessEndTime?: Date | null; + permissions: TProjectPermission[]; + isApproved: boolean; + } | null; + + policy: { + id: string; + name: string; + approvals: number; + approvers: string[]; + secretPath?: string | null; + envId: string; + }; + + reviewers: { + member: string; + status: string; + }[]; +}; + +export type TAccessApproval = { + id: string; + policyId: string; + privilegeId: string; + requestedBy: string; +}; + +export type TAccessRequestCount = { + pendingCount: number; + finalizedCount: number; +}; + +export type TProjectUserPrivilege = { + projectMembershipId: string; + slug: string; + id: string; + createdAt: Date; + updatedAt: Date; + permissions?: TProjectPermission[]; +} & ( + | { + isTemporary: true; + temporaryMode: string; + temporaryRange: string; + temporaryAccessStartTime: string; + temporaryAccessEndTime?: string; + } + | { + isTemporary: false; + temporaryMode?: null; + temporaryRange?: null; + temporaryAccessStartTime?: null; + temporaryAccessEndTime?: null; + } +); + +export type TCreateAccessRequestDTO = { + envSlug: string; + projectSlug: string; + secretPath: string; +} & Omit; + +export type TGetAccessApprovalRequestsDTO = { + projectSlug: string; + envSlug?: string; + authorProjectMembershipId?: string; +}; + +export type TGetAccessPolicyApprovalCountDTO = { + projectSlug: string; + envSlug: string; }; export type TGetSecretApprovalPolicyOfBoardDTO = { @@ -22,24 +115,27 @@ export type TGetSecretApprovalPolicyOfBoardDTO = { }; export type TCreateAccessPolicyDTO = { - workspaceId: string; + projectSlug: string; name?: string; environment: string; approvers?: string[]; approvals?: number; + secretPath?: string; }; export type TUpdateAccessPolicyDTO = { id: string; name?: string; approvers?: string[]; + secretPath?: string; + environment?: string; approvals?: number; // for invalidating list - workspaceId: string; + projectSlug: string; }; export type TDeleteSecretPolicyDTO = { id: string; // for invalidating list - workspaceId: string; + projectSlug: string; };