feat(secret-approval): added auto naming policy and minor ux enhancements

This commit is contained in:
Akhil Mohan
2023-10-03 22:37:01 +05:30
parent 4fd227c85f
commit 9dc97f7208
13 changed files with 94 additions and 30 deletions

View File

@@ -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 })
});

View File

@@ -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<ISecretApprovalPolicy>(
ref: "Membership"
}
],
name: {
type: String
},
environment: {
type: String,
required: true

View File

@@ -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({

View File

@@ -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({
</div>
);
})}
{isProtectedBranch && <FontAwesomeIcon icon={faShield} className="text-primary" />}
{isProtectedBranch && (
<Tooltip content={`Protected by policy ${protectionPolicyName}`}>
<FontAwesomeIcon icon={faLock} className="text-primary ml-2" />
</Tooltip>
)}
</div>
);
}

View File

@@ -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 = ({

View File

@@ -1,6 +1,7 @@
export type TSecretApprovalPolicy = {
_id: string;
workspace: string;
name: string;
environment: string;
secretPath?: string;
approvers: string[];

View File

@@ -47,6 +47,7 @@ export type TSecretApprovalRequest<
J extends unknown = EncryptedSecret
> = {
_id: string;
createdAt: string;
committer: string;
reviewers: {
member: string;

View File

@@ -87,6 +87,7 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Environment</Th>
<Th>Secret Path</Th>
<Th>Eligible Approvers</Th>

View File

@@ -38,6 +38,7 @@ export const SecretApprovalPolicyRow = ({
return (
<Tr>
<Td>{policy.name}</Td>
<Td>{policy.environment}</Td>
<Td>{policy.secretPath || "*"}</Td>
<Td>

View File

@@ -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<typeof formSchema>;
@@ -126,6 +132,15 @@ export const SecretPolicyForm = ({
<Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Policy Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="environment"

View File

@@ -6,6 +6,7 @@ import {
faCodeBranch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion";
import {
@@ -18,9 +19,9 @@ import {
EmptyState,
Skeleton
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useUser, useWorkspace } from "@app/context";
import { useGetSecretApprovalRequests, useGetWorkspaceUsers } from "@app/hooks/api";
import { TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
import {
generateCommitText,
@@ -50,11 +51,13 @@ export const SecretApprovalRequest = () => {
environment: envFilter,
committer: committerFilter
});
const { user: presentUser } = useUser();
const { data: members } = useGetWorkspaceUsers(workspaceId);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(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) => (
<Fragment key={`secret-approval-request-${i + 1}`}>
{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 (
<div
key={reqId}
@@ -198,9 +215,11 @@ export const SecretApprovalRequest = () => {
{generateCommitText(commits)}
</div>
<span className="text-xs text-gray-500">
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"}
</span>
</div>
);

View File

@@ -216,6 +216,7 @@ export const SecretMainPage = () => {
isProjectRelated
onEnvChange={handleEnvChange}
isProtectedBranch={isProtectedBranch}
protectionPolicyName={boardPolicy?.name}
/>
</div>
{!isRollbackMode ? (

View File

@@ -218,7 +218,7 @@ export const SecretListView = ({
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
if (isProtectedBranch) cb();
if (cb) cb();
}
queryClient.invalidateQueries(