mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
feat(secret-approval): added auto naming policy and minor ux enhancements
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
@@ -15,7 +16,7 @@ const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approva
|
|||||||
|
|
||||||
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
body: { approvals, secretPath, approvers, environment, workspaceId }
|
body: { approvals, secretPath, approvers, environment, workspaceId, name }
|
||||||
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
||||||
|
|
||||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||||
@@ -26,6 +27,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) =>
|
|||||||
|
|
||||||
const secretApproval = new SecretApprovalPolicy({
|
const secretApproval = new SecretApprovalPolicy({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
|
name: name ?? `${environment}-${nanoid(3)}`,
|
||||||
secretPath,
|
secretPath,
|
||||||
environment,
|
environment,
|
||||||
approvals,
|
approvals,
|
||||||
@@ -40,7 +42,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) =>
|
|||||||
|
|
||||||
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
body: { approvals, approvers, secretPath },
|
body: { approvals, approvers, secretPath, name },
|
||||||
params: { id }
|
params: { id }
|
||||||
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ export const updateSecretApprovalPolicy = async (req: Request, res: Response) =>
|
|||||||
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
|
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
|
||||||
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose";
|
|||||||
export interface ISecretApprovalPolicy {
|
export interface ISecretApprovalPolicy {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvers: Types.ObjectId[];
|
approvers: Types.ObjectId[];
|
||||||
@@ -23,6 +24,9 @@ const secretApprovalPolicySchema = new Schema<ISecretApprovalPolicy>(
|
|||||||
ref: "Membership"
|
ref: "Membership"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
name: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -15,24 +15,36 @@ export const GetSecretApprovalPolicyOfABoard = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const CreateSecretApprovalRule = z.object({
|
export const CreateSecretApprovalRule = z.object({
|
||||||
body: z.object({
|
body: z
|
||||||
|
.object({
|
||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
approvers: z.string().array().optional(),
|
approvers: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(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({
|
export const UpdateSecretApprovalRule = z.object({
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
approvers: z.string().array().optional(),
|
.object({
|
||||||
approvals: z.number().min(1).optional(),
|
name: z.string().optional(),
|
||||||
|
approvers: z.string().array().min(1),
|
||||||
|
approvals: z.number().min(1).default(1),
|
||||||
secretPath: z.string().optional().nullable()
|
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({
|
export const DeleteSecretApprovalRule = z.object({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
@@ -17,6 +17,7 @@ type Props = {
|
|||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
isFolderMode?: boolean;
|
isFolderMode?: boolean;
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
|
protectionPolicyName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: make links clickable and clean up
|
// TODO: make links clickable and clean up
|
||||||
@@ -44,7 +45,8 @@ export default function NavHeader({
|
|||||||
onEnvChange,
|
onEnvChange,
|
||||||
isFolderMode,
|
isFolderMode,
|
||||||
secretPath = "/",
|
secretPath = "/",
|
||||||
isProtectedBranch = false
|
isProtectedBranch = false,
|
||||||
|
protectionPolicyName
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -153,7 +155,11 @@ export default function NavHeader({
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const fetchApprovalPolicyOfABoard = async (
|
|||||||
"/api/v1/secret-approvals/board",
|
"/api/v1/secret-approvals/board",
|
||||||
{ params: { workspaceId, environment, secretPath } }
|
{ params: { workspaceId, environment, secretPath } }
|
||||||
);
|
);
|
||||||
return data.policy;
|
return data.policy || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetSecretApprovalPolicyOfABoard = ({
|
export const useGetSecretApprovalPolicyOfABoard = ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type TSecretApprovalPolicy = {
|
export type TSecretApprovalPolicy = {
|
||||||
_id: string;
|
_id: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
|
name: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvers: string[];
|
approvers: string[];
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type TSecretApprovalRequest<
|
|||||||
J extends unknown = EncryptedSecret
|
J extends unknown = EncryptedSecret
|
||||||
> = {
|
> = {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
createdAt: string;
|
||||||
committer: string;
|
committer: string;
|
||||||
reviewers: {
|
reviewers: {
|
||||||
member: string;
|
member: string;
|
||||||
|
|||||||
+1
@@ -87,6 +87,7 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
<Th>Environment</Th>
|
<Th>Environment</Th>
|
||||||
<Th>Secret Path</Th>
|
<Th>Secret Path</Th>
|
||||||
<Th>Eligible Approvers</Th>
|
<Th>Eligible Approvers</Th>
|
||||||
|
|||||||
+1
@@ -38,6 +38,7 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr>
|
<Tr>
|
||||||
|
<Td>{policy.name}</Td>
|
||||||
<Td>{policy.environment}</Td>
|
<Td>{policy.environment}</Td>
|
||||||
<Td>{policy.secretPath || "*"}</Td>
|
<Td>{policy.secretPath || "*"}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
+17
-2
@@ -33,11 +33,17 @@ type Props = {
|
|||||||
editValues?: TSecretApprovalPolicy;
|
editValues?: TSecretApprovalPolicy;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z
|
||||||
|
.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
approvals: z.number().min(1),
|
approvals: z.number().min(1),
|
||||||
approvers: z.string().array().optional()
|
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>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
@@ -126,6 +132,15 @@ export const SecretPolicyForm = ({
|
|||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
|
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="environment"
|
name="environment"
|
||||||
|
|||||||
+24
-5
@@ -6,6 +6,7 @@ import {
|
|||||||
faCodeBranch
|
faCodeBranch
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { formatDistance } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,9 +19,9 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
Skeleton
|
Skeleton
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useUser, useWorkspace } from "@app/context";
|
||||||
import { useGetSecretApprovalRequests, useGetWorkspaceUsers } from "@app/hooks/api";
|
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 {
|
import {
|
||||||
generateCommitText,
|
generateCommitText,
|
||||||
@@ -50,11 +51,13 @@ export const SecretApprovalRequest = () => {
|
|||||||
environment: envFilter,
|
environment: envFilter,
|
||||||
committer: committerFilter
|
committer: committerFilter
|
||||||
});
|
});
|
||||||
|
const { user: presentUser } = useUser();
|
||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
||||||
(prev, curr) => ({ ...prev, [curr._id]: curr }),
|
(prev, curr) => ({ ...prev, [curr._id]: curr }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
const myMembershipId = members?.find(({ user }) => user._id === presentUser._id)?._id;
|
||||||
const isSecretApprovalScreen = Boolean(selectedApproval);
|
const isSecretApprovalScreen = Boolean(selectedApproval);
|
||||||
|
|
||||||
const handleGoBackSecretRequestDetail = () => {
|
const handleGoBackSecretRequestDetail = () => {
|
||||||
@@ -181,7 +184,21 @@ export const SecretApprovalRequest = () => {
|
|||||||
{secretApprovalRequests?.pages?.map((group, i) => (
|
{secretApprovalRequests?.pages?.map((group, i) => (
|
||||||
<Fragment key={`secret-approval-request-${i + 1}`}>
|
<Fragment key={`secret-approval-request-${i + 1}`}>
|
||||||
{group?.map((secretApproval) => {
|
{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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={reqId}
|
key={reqId}
|
||||||
@@ -198,9 +215,11 @@ export const SecretApprovalRequest = () => {
|
|||||||
{generateCommitText(commits)}
|
{generateCommitText(commits)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">
|
<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?.lastName} (
|
||||||
{membersGroupById?.[committer]?.user?.email}) - Review required
|
{membersGroupById?.[committer]?.user?.email}){" "}
|
||||||
|
{isApprover && !isReviewed && status === "open" && "- Review required"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ export const SecretMainPage = () => {
|
|||||||
isProjectRelated
|
isProjectRelated
|
||||||
onEnvChange={handleEnvChange}
|
onEnvChange={handleEnvChange}
|
||||||
isProtectedBranch={isProtectedBranch}
|
isProtectedBranch={isProtectedBranch}
|
||||||
|
protectionPolicyName={boardPolicy?.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isRollbackMode ? (
|
{!isRollbackMode ? (
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export const SecretListView = ({
|
|||||||
newKey: hasKeyChanged ? key : undefined,
|
newKey: hasKeyChanged ? key : undefined,
|
||||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||||
});
|
});
|
||||||
if (isProtectedBranch) cb();
|
if (cb) cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
|
|||||||
Reference in New Issue
Block a user