diff --git a/backend/src/controllers/v1/secretApprovalPolicyController.ts b/backend/src/controllers/v1/secretApprovalPolicyController.ts index c9afe2af4..2f8e19acb 100644 --- a/backend/src/controllers/v1/secretApprovalPolicyController.ts +++ b/backend/src/controllers/v1/secretApprovalPolicyController.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; +import { nanoid } from "nanoid"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -15,7 +16,7 @@ const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approva export const createSecretApprovalPolicy = async (req: Request, res: Response) => { const { - body: { approvals, secretPath, approvers, environment, workspaceId } + body: { approvals, secretPath, approvers, environment, workspaceId, name } } = await validateRequest(reqValidator.CreateSecretApprovalRule, req); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); @@ -26,6 +27,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) => const secretApproval = new SecretApprovalPolicy({ workspace: workspaceId, + name: name ?? `${environment}-${nanoid(3)}`, secretPath, environment, approvals, @@ -40,7 +42,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) => export const updateSecretApprovalPolicy = async (req: Request, res: Response) => { const { - body: { approvals, approvers, secretPath }, + body: { approvals, approvers, secretPath, name }, params: { id } } = await validateRequest(reqValidator.UpdateSecretApprovalRule, req); @@ -59,6 +61,7 @@ export const updateSecretApprovalPolicy = async (req: Request, res: Response) => const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, { approvals, approvers, + name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`, ...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath }) }); diff --git a/backend/src/models/secretApprovalPolicy.ts b/backend/src/models/secretApprovalPolicy.ts index 9471f82db..376b541c7 100644 --- a/backend/src/models/secretApprovalPolicy.ts +++ b/backend/src/models/secretApprovalPolicy.ts @@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose"; export interface ISecretApprovalPolicy { _id: Types.ObjectId; workspace: Types.ObjectId; + name: string; environment: string; secretPath?: string; approvers: Types.ObjectId[]; @@ -23,6 +24,9 @@ const secretApprovalPolicySchema = new Schema( ref: "Membership" } ], + name: { + type: String + }, environment: { type: String, required: true diff --git a/backend/src/validation/secretApproval.ts b/backend/src/validation/secretApproval.ts index 1d2b1177d..81d50fcc1 100644 --- a/backend/src/validation/secretApproval.ts +++ b/backend/src/validation/secretApproval.ts @@ -15,24 +15,36 @@ export const GetSecretApprovalPolicyOfABoard = z.object({ }); export const CreateSecretApprovalRule = z.object({ - body: z.object({ - workspaceId: z.string(), - environment: z.string(), - secretPath: z.string().optional().nullable(), - approvers: z.string().array().optional(), - approvals: z.number().min(1).default(1) - }) + body: z + .object({ + workspaceId: z.string(), + name: z.string().optional(), + environment: z.string(), + secretPath: z.string().optional().nullable(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "Approvals should be lower than approvals" + }) }); export const UpdateSecretApprovalRule = z.object({ params: z.object({ id: z.string() }), - body: z.object({ - approvers: z.string().array().optional(), - approvals: z.number().min(1).optional(), - secretPath: z.string().optional().nullable() - }) + body: z + .object({ + name: z.string().optional(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1), + secretPath: z.string().optional().nullable() + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "Approvals should be lower than approvals" + }) }); export const DeleteSecretApprovalRule = z.object({ diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 2edc1b01e..d2a046139 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { useRouter } from "next/router"; -import { faAngleRight, faShield } from "@fortawesome/free-solid-svg-icons"; +import { faAngleRight, faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useOrganization, useWorkspace } from "@app/context"; @@ -17,6 +17,7 @@ type Props = { secretPath?: string; isFolderMode?: boolean; isProtectedBranch?: boolean; + protectionPolicyName?: string; }; // TODO: make links clickable and clean up @@ -44,7 +45,8 @@ export default function NavHeader({ onEnvChange, isFolderMode, secretPath = "/", - isProtectedBranch = false + isProtectedBranch = false, + protectionPolicyName }: Props): JSX.Element { const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization(); @@ -153,7 +155,11 @@ export default function NavHeader({ ); })} - {isProtectedBranch && } + {isProtectedBranch && ( + + + + )} ); } diff --git a/frontend/src/hooks/api/secretApproval/queries.tsx b/frontend/src/hooks/api/secretApproval/queries.tsx index 4b176e073..ae90ff766 100644 --- a/frontend/src/hooks/api/secretApproval/queries.tsx +++ b/frontend/src/hooks/api/secretApproval/queries.tsx @@ -52,7 +52,7 @@ const fetchApprovalPolicyOfABoard = async ( "/api/v1/secret-approvals/board", { params: { workspaceId, environment, secretPath } } ); - return data.policy; + return data.policy || ""; }; export const useGetSecretApprovalPolicyOfABoard = ({ diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index abf98d706..f82d605e5 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -1,6 +1,7 @@ export type TSecretApprovalPolicy = { _id: string; workspace: string; + name: string; environment: string; secretPath?: string; approvers: string[]; diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index c73394570..12f2ce81f 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -47,6 +47,7 @@ export type TSecretApprovalRequest< J extends unknown = EncryptedSecret > = { _id: string; + createdAt: string; committer: string; reviewers: { member: string; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index 333541c01..345a3efb4 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -87,6 +87,7 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { + diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx index c39ba2b69..c023d4e03 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -38,6 +38,7 @@ export const SecretApprovalPolicyRow = ({ return ( +
Name Environment Secret Path Eligible Approvers
{policy.name} {policy.environment} {policy.secretPath || "*"} diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx index 0b8048c53..7db296398 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx @@ -33,12 +33,18 @@ type Props = { editValues?: TSecretApprovalPolicy; }; -const formSchema = z.object({ - environment: z.string(), - secretPath: z.string().optional().nullable(), - approvals: z.number().min(1), - approvers: z.string().array().optional() -}); +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional().nullable(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "Approvals should be lower than approvals" + }); type TFormSchema = z.infer; @@ -126,6 +132,15 @@ export const SecretPolicyForm = ({
+ ( + + + + )} + /> { environment: envFilter, committer: committerFilter }); + const { user: presentUser } = useUser(); const { data: members } = useGetWorkspaceUsers(workspaceId); const membersGroupById = members?.reduce>( (prev, curr) => ({ ...prev, [curr._id]: curr }), {} ); + const myMembershipId = members?.find(({ user }) => user._id === presentUser._id)?._id; const isSecretApprovalScreen = Boolean(selectedApproval); const handleGoBackSecretRequestDetail = () => { @@ -181,7 +184,21 @@ export const SecretApprovalRequest = () => { {secretApprovalRequests?.pages?.map((group, i) => ( {group?.map((secretApproval) => { - const { _id: reqId, commits, committer } = secretApproval; + const { + _id: reqId, + commits, + committer, + createdAt, + policy, + reviewers, + status + } = secretApproval; + const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1; + const isReviewed = + reviewers.findIndex( + ({ member, status: reviewStatus }) => + member === myMembershipId && reviewStatus === ApprovalStatus.APPROVED + ) !== -1; return (
{ {generateCommitText(commits)}
- Opened 2 hours ago by {membersGroupById?.[committer]?.user?.firstName}{" "} + Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} + {membersGroupById?.[committer]?.user?.firstName}{" "} {membersGroupById?.[committer]?.user?.lastName} ( - {membersGroupById?.[committer]?.user?.email}) - Review required + {membersGroupById?.[committer]?.user?.email}){" "} + {isApprover && !isReviewed && status === "open" && "- Review required"} ); diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index fc7a45fc3..4ffa27c86 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -216,6 +216,7 @@ export const SecretMainPage = () => { isProjectRelated onEnvChange={handleEnvChange} isProtectedBranch={isProtectedBranch} + protectionPolicyName={boardPolicy?.name} /> {!isRollbackMode ? ( diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx index 7ea9e24c4..431f6be7f 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx @@ -218,7 +218,7 @@ export const SecretListView = ({ newKey: hasKeyChanged ? key : undefined, skipMultilineEncoding: modSecret.skipMultilineEncoding }); - if (isProtectedBranch) cb(); + if (cb) cb(); } queryClient.invalidateQueries(