feat(policies): Bypass Approval Rework

This commit is contained in:
x032205
2025-05-27 20:46:46 -04:00
parent e69354b546
commit 3190ff2eb1
41 changed files with 1109 additions and 251 deletions
@@ -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) => (
@@ -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
@@ -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) => (