mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 17:28:13 +00:00
feat(policies): Bypass Approval Rework
This commit is contained in:
+6
-10
@@ -35,7 +35,7 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionApprovalActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useDeleteAccessApprovalPolicy,
|
||||
@@ -61,10 +61,8 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
|
||||
projectSlug: currentWorkspace?.slug as string,
|
||||
options: {
|
||||
enabled:
|
||||
permission.can(
|
||||
ProjectPermissionApprovalActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
) && !!currentWorkspace?.slug
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||
!!currentWorkspace?.slug
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -73,10 +71,8 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
|
||||
workspaceId: currentWorkspace?.id as string,
|
||||
options: {
|
||||
enabled:
|
||||
permission.can(
|
||||
ProjectPermissionApprovalActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
) && !!currentWorkspace?.id
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||
!!currentWorkspace?.id
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -164,7 +160,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionApprovalActions.Create}
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
||||
+179
-52
@@ -27,7 +27,11 @@ import {
|
||||
useCreateAccessApprovalPolicy,
|
||||
useUpdateAccessApprovalPolicy
|
||||
} from "@app/hooks/api/accessApproval";
|
||||
import { ApproverType, TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
|
||||
import {
|
||||
ApproverType,
|
||||
BypasserType,
|
||||
TAccessApprovalPolicy
|
||||
} from "@app/hooks/api/accessApproval/types";
|
||||
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||
|
||||
@@ -54,6 +58,14 @@ const formSchema = z
|
||||
.object({ type: z.literal(ApproverType.Group), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
userBypassers: z
|
||||
.object({ type: z.literal(BypasserType.User), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
groupBypassers: z
|
||||
.object({ type: z.literal(BypasserType.Group), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
policyType: z.nativeEnum(PolicyType),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
@@ -103,6 +115,14 @@ export const AccessPolicyForm = ({
|
||||
editValues?.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
|
||||
userBypassers:
|
||||
editValues?.bypassers
|
||||
?.filter((bypasser) => bypasser.type === BypasserType.User)
|
||||
.map(({ id, type }) => ({ id, type: type as BypasserType.User })) || [],
|
||||
groupBypassers:
|
||||
editValues?.bypassers
|
||||
?.filter((bypasser) => bypasser.type === BypasserType.Group)
|
||||
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
||||
approvals: editValues?.approvals,
|
||||
allowedSelfApprovals: editValues?.allowedSelfApprovals
|
||||
}
|
||||
@@ -125,20 +145,30 @@ export const AccessPolicyForm = ({
|
||||
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
|
||||
|
||||
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
||||
const enforcementLevel = watch("enforcementLevel");
|
||||
|
||||
const formUserBypassers = watch("userBypassers");
|
||||
const formGroupBypassers = watch("groupBypassers");
|
||||
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
|
||||
|
||||
const handleCreatePolicy = async ({
|
||||
environment,
|
||||
groupApprovers,
|
||||
userApprovers,
|
||||
groupBypassers,
|
||||
userBypassers,
|
||||
...data
|
||||
}: TFormSchema) => {
|
||||
if (!projectId) return;
|
||||
|
||||
try {
|
||||
const bypassers = [...userBypassers, ...groupBypassers];
|
||||
|
||||
if (data.policyType === PolicyType.ChangePolicy) {
|
||||
await createSecretApprovalPolicy({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
@@ -146,6 +176,7 @@ export const AccessPolicyForm = ({
|
||||
await createAccessApprovalPolicy({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
projectSlug
|
||||
});
|
||||
@@ -168,17 +199,22 @@ export const AccessPolicyForm = ({
|
||||
environment,
|
||||
userApprovers,
|
||||
groupApprovers,
|
||||
userBypassers,
|
||||
groupBypassers,
|
||||
...data
|
||||
}: TFormSchema) => {
|
||||
if (!projectId || !projectSlug) return;
|
||||
if (!editValues?.id) return;
|
||||
|
||||
try {
|
||||
const bypassers = [...userBypassers, ...groupBypassers];
|
||||
|
||||
if (data.policyType === PolicyType.ChangePolicy) {
|
||||
await updateSecretApprovalPolicy({
|
||||
id: editValues?.id,
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
} else {
|
||||
@@ -186,6 +222,7 @@ export const AccessPolicyForm = ({
|
||||
id: editValues?.id,
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
projectSlug
|
||||
});
|
||||
@@ -230,6 +267,24 @@ export const AccessPolicyForm = ({
|
||||
[groups]
|
||||
);
|
||||
|
||||
const bypasserMemberOptions = useMemo(
|
||||
() =>
|
||||
members.map((member) => ({
|
||||
id: member.user.id,
|
||||
type: BypasserType.User
|
||||
})),
|
||||
[members]
|
||||
);
|
||||
|
||||
const bypasserGroupOptions = useMemo(
|
||||
() =>
|
||||
groups?.map(({ group }) => ({
|
||||
id: group.id,
|
||||
type: BypasserType.Group
|
||||
})),
|
||||
[groups]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent
|
||||
@@ -345,58 +400,62 @@ export const AccessPolicyForm = ({
|
||||
Select members or groups that are allowed to approve requests from this policy.
|
||||
</p>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members that are allowed to approve requests..."
|
||||
options={memberOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => {
|
||||
const member = members?.find((m) => m.user.id === option.id);
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="userApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members..."
|
||||
options={memberOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => {
|
||||
const member = members?.find((m) => m.user.id === option.id);
|
||||
|
||||
if (!member) return option.id;
|
||||
if (!member) return option.id;
|
||||
|
||||
return getMemberLabel(member);
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups that are allowed to approve requests..."
|
||||
options={groupOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) =>
|
||||
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
return getMemberLabel(member);
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups..."
|
||||
options={groupOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) =>
|
||||
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedSelfApprovals"
|
||||
@@ -427,6 +486,7 @@ export const AccessPolicyForm = ({
|
||||
label="Bypass Approvals"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-3"
|
||||
>
|
||||
<Switch
|
||||
id="bypass-approvals"
|
||||
@@ -436,11 +496,78 @@ export const AccessPolicyForm = ({
|
||||
onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)
|
||||
}
|
||||
>
|
||||
Allow request creators to bypass policy in break-glass situations
|
||||
Allow certain users to bypass policy in break-glass situations
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{enforcementLevel === EnforcementLevel.Soft && (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="userBypassers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Bypassers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2 w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members..."
|
||||
options={bypasserMemberOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => {
|
||||
const member = members?.find((m) => m.user.id === option.id);
|
||||
|
||||
if (!member) return option.id;
|
||||
|
||||
return getMemberLabel(member);
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupBypassers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Bypassers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2 w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups..."
|
||||
options={bypasserGroupOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) =>
|
||||
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
||||
option.id
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bypasserCount <= 0 && (
|
||||
<span className="text-sm text-red-500">
|
||||
Not selecting specific users or groups will allow anyone to bypass this policy
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Save
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionApprovalActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
import { policyDetails } from "@app/helpers/policies";
|
||||
import { Approver } from "@app/hooks/api/accessApproval/types";
|
||||
@@ -118,7 +118,7 @@ export const ApprovalPolicyRow = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionApprovalActions.Edit}
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -137,7 +137,7 @@ export const ApprovalPolicyRow = ({
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionApprovalActions.Delete}
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
||||
Reference in New Issue
Block a user