diff --git a/backend/src/controllers/v1/secretApprovalRequestsController.ts b/backend/src/controllers/v1/secretApprovalRequestsController.ts index 47fb43fa2..33e84c07e 100644 --- a/backend/src/controllers/v1/secretApprovalRequestsController.ts +++ b/backend/src/controllers/v1/secretApprovalRequestsController.ts @@ -6,8 +6,9 @@ import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprov import * as reqValidator from "../../validation/secretApprovalRequest"; import { getFolderWithPathFromId } from "../../services/FolderService"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; -import { ISecretApprovalPolicy } from "../../models/secretApprovalPolicy"; +import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy"; import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService"; +import { Types } from "mongoose"; export const getSecretApprovalRequests = async (req: Request, res: Response) => { const { @@ -17,24 +18,43 @@ export const getSecretApprovalRequests = async (req: Request, res: Response) => const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); const query = { - workspace: workspaceId, + workspace: new Types.ObjectId(workspaceId), environment, - committer, - status, - ...(membership.role !== "admin" - ? { $or: [{ committer: membership.id }, { "policy.approvers": membership.id }] } - : {}) + committer: committer ? new Types.ObjectId(committer) : undefined, + status }; // to strip of undefined in query we use es6 spread to ignore those fields Object.entries(query).forEach( ([key, value]) => value === undefined && delete query[key as keyof typeof query] ); - const approvalRequests = await SecretApprovalRequest.find(query) - .sort({ createdAt: -1 }) - .limit(limit) - .skip(offset) - .populate("policy") - .lean(); + const approvalRequests = await SecretApprovalRequest.aggregate([ + { + $match: query + }, + { + $lookup: { + from: SecretApprovalPolicy.collection.name, + localField: "policy", + foreignField: "_id", + as: "policy" + } + }, + { $unwind: "$policy" }, + ...(membership.role !== "admin" + ? [ + { + $match: { + $or: [ + { committer: new Types.ObjectId(membership.id) }, + { "policy.approvers": new Types.ObjectId(membership.id) } + ] + } + } + ] + : []), + { $skip: offset }, + { $limit: limit } + ]); if (!approvalRequests.length) return res.send({ approvals: [] }); const unqiueEnvs = environment ?? { @@ -114,7 +134,7 @@ export const updateSecretApprovalReviewStatus = async (req: Request, res: Respon if ( membership.role !== "admin" && secretApprovalRequest.committer !== membership.id && - !secretApprovalRequest.policy.approvers.find((approverId) => approverId === membership.id) + !secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id)) ) { throw UnauthorizedRequestError({ message: "User has no access" }); } diff --git a/backend/src/services/SecretApprovalService.ts b/backend/src/services/SecretApprovalService.ts index 4e4ba5bb7..ca38e3279 100644 --- a/backend/src/services/SecretApprovalService.ts +++ b/backend/src/services/SecretApprovalService.ts @@ -42,7 +42,7 @@ export const getSecretPolicyOfBoard = async ( // now sort by priority. exact secret path gets first match followed by glob followed by just env scoped // if that is tie get by first createdAt const policiesByPriority = policiesFilteredByPath.sort( - (a, b) => getPolicyScore(a) - getPolicyScore(b) + (a, b) => getPolicyScore(b) - getPolicyScore(a) ); const finalPolicy = policiesByPriority.shift(); return finalPolicy; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 9a2e89713..ae4b49d63 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -20,7 +20,8 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Workspace = "workspace", Secrets = "secrets", - SecretRollback = "secret-rollback" + SecretRollback = "secret-rollback", + SecretApproval = "secret-approval" } type SubjectFields = { @@ -43,6 +44,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index f171636a0..e0a8df95d 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -9,13 +9,14 @@ export const useCreateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateSecretPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath }) => { + mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, approvers, - secretPath + secretPath, + name }); return data; }, @@ -29,11 +30,12 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, secretPath }) => { + mutationFn: async ({ id, approvers, approvals, secretPath, name }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, approvers, - secretPath + secretPath, + name }); return data; }, diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index f82d605e5..0095d699c 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -20,6 +20,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = { export type TCreateSecretPolicyDTO = { workspaceId: string; + name?: string; environment: string; secretPath?: string | null; approvers?: string[]; @@ -28,6 +29,7 @@ export type TCreateSecretPolicyDTO = { export type TUpdateSecretPolicyDTO = { id: string; + name?: string; approvers?: string[]; secretPath?: string | null; approvals?: number; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index 0dbbfa85e..dbfd6ec03 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -9,6 +9,7 @@ import { faLock, faNetworkWired, faPuzzlePiece, + faShield, faTags, faUser, faUsers @@ -18,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, FormControl, Input, UpgradePlanModal } from "@app/components/v2"; -import { useOrganization, useSubscription, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useOrganization, useSubscription, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateRole, useUpdateRole } from "@app/hooks/api"; import { TRole } from "@app/hooks/api/roles/types"; @@ -41,6 +42,12 @@ const SINGLE_PERMISSION_LIST = [ icon: faPuzzlePiece, formName: "integrations" }, + { + title: "Secret Protect policy", + subtitle: "Manage policies for secret protection for unauthorized secret changes", + icon: faShield, + formName: ProjectPermissionSub.SecretApproval + }, { title: "Roles", subtitle: "Role management control", diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index f81a0f295..fec02934c 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ import { z } from "zod"; +import { ProjectPermissionSub } from "@app/context"; import { TProjectPermission } from "@app/hooks/api/roles/types"; const generalPermissionSchema = z @@ -41,6 +42,8 @@ export const formSchema = z.object({ tags: generalPermissionSchema, "audit-logs": generalPermissionSchema, "ip-allowlist": generalPermissionSchema, + // akhilmhdh: refactor all keys like below + [ProjectPermissionSub.SecretApproval]: generalPermissionSchema, workspace: z .object({ edit: z.boolean().optional(), diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx index 884d7632a..54a58576f 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx @@ -6,6 +6,7 @@ import { motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { Checkbox, Select, SelectItem } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { TFormSchema } from "./ProjectRoleModifySection.utils"; @@ -21,7 +22,8 @@ type Props = { | "environments" | "tags" | "audit-logs" - | "ip-allowlist"; + | "ip-allowlist" + | ProjectPermissionSub.SecretApproval; isNonEditable?: boolean; setValue: UseFormSetValue; control: Control; diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index f5c8fef8a..c6e229970 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -14,9 +14,9 @@ export const SecretApprovalPage = () => { const workspaceId = currentWorkspace?._id || ""; return ( -
+
-

Admin Panels

+

Secret Approvals

diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index 345a3efb4..a15199d5e 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -2,6 +2,7 @@ import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -15,6 +16,7 @@ import { THead, Tr } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteSecretApprovalPolicy, @@ -35,11 +37,15 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { "secretPolicyForm", "deletePolicy" ] as const); + const permission = useProjectPermission(); const { createNotification } = useNotificationContext(); const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({ - workspaceId + workspaceId, + options: { + enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) + } }); const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); @@ -75,12 +81,20 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
- + {(isAllowed) => ( + + )} +
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx index c023d4e03..fbf9598d9 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { DropdownMenu, DropdownMenuContent, @@ -11,9 +12,9 @@ import { IconButton, Input, Td, - Tooltip, Tr } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { useUpdateSecretApprovalPolicy } from "@app/hooks/api"; import { TSecretApprovalPolicy } from "@app/hooks/api/types"; import { TWorkspaceUser } from "@app/hooks/api/users/types"; @@ -35,6 +36,7 @@ export const SecretApprovalPolicyRow = ({ }: Props) => { const [selectedApprovers, setSelectedApprovers] = useState([]); const { mutate: updateSecretApprovalPolicy, isLoading } = useUpdateSecretApprovalPolicy(); + const permission = useProjectPermission(); return ( @@ -62,7 +64,13 @@ export const SecretApprovalPolicyRow = ({ } }} > - + {policy.approvals}
- - - - - - - - - - + + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} +
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index a566eca21..4757cceb0 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -19,6 +19,7 @@ export type Props = { secretVersion?: DecryptedSecret; newVersion?: Omit & { tags?: WsTag[] }; presentSecretVersionNumber: number; + hasMerged?: Boolean; }; const generateItemTitle = (op: CommitType) => { @@ -38,10 +39,11 @@ export const SecretApprovalRequestChangeItem = ({ op, secretVersion, newVersion, - presentSecretVersionNumber + presentSecretVersionNumber, + hasMerged }: Props) => { // meaning request has changed - const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber; + const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber && !hasMerged; return (
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 1a361dddc..fcaa422fe 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -211,6 +211,7 @@ export const SecretApprovalRequestChanges = ({ ({ op, secretVersion, secret, newVersion }, index) => (