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
@@ -2,7 +2,6 @@ export { useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types";
export {
ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -24,15 +24,6 @@ export enum ProjectPermissionSecretActions {
Delete = "delete"
}
export enum ProjectPermissionApprovalActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential",
@@ -294,7 +285,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [
ProjectPermissionIdentityActions,
(
-1
View File
@@ -10,7 +10,6 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -21,6 +21,7 @@ export const useCreateAccessApprovalPolicy = () => {
projectSlug,
approvals,
approvers,
bypassers,
name,
secretPath,
enforcementLevel,
@@ -30,6 +31,7 @@ export const useCreateAccessApprovalPolicy = () => {
environment,
projectSlug,
approvals,
bypassers,
approvers,
secretPath,
name,
@@ -53,6 +55,7 @@ export const useUpdateAccessApprovalPolicy = () => {
mutationFn: async ({
id,
approvers,
bypassers,
approvals,
name,
secretPath,
@@ -62,6 +65,7 @@ export const useUpdateAccessApprovalPolicy = () => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -16,6 +16,7 @@ export type TAccessApprovalPolicy = {
enforcementLevel: EnforcementLevel;
updatedAt: Date;
approvers?: Approver[];
bypassers?: Bypasser[];
allowedSelfApprovals: boolean;
};
@@ -24,11 +25,21 @@ export enum ApproverType {
Group = "group"
}
export enum BypasserType {
User = "user",
Group = "group"
}
export type Approver = {
id: string;
type: ApproverType;
};
export type Bypasser = {
id: string;
type: BypasserType;
};
export type TAccessApprovalRequest = {
id: string;
policyId: string;
@@ -68,6 +79,7 @@ export type TAccessApprovalRequest = {
name: string;
approvals: number;
approvers: string[];
bypassers: string[];
secretPath?: string | null;
envId: string;
enforcementLevel: EnforcementLevel;
@@ -146,6 +158,7 @@ export type TCreateAccessPolicyDTO = {
name?: string;
environment: string;
approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number;
secretPath?: string;
enforcementLevel?: EnforcementLevel;
@@ -156,6 +169,7 @@ export type TUpdateAccessPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string;
environment?: string;
approvals?: number;
@@ -14,6 +14,7 @@ export const useCreateSecretApprovalPolicy = () => {
workspaceId,
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -24,6 +25,7 @@ export const useCreateSecretApprovalPolicy = () => {
workspaceId,
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -46,6 +48,7 @@ export const useUpdateSecretApprovalPolicy = () => {
mutationFn: async ({
id,
approvers,
bypassers,
approvals,
secretPath,
name,
@@ -55,6 +58,7 @@ export const useUpdateSecretApprovalPolicy = () => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -25,6 +25,16 @@ export type Approver = {
type: ApproverType;
};
export enum BypasserType {
User = "user",
Group = "group"
}
export type Bypasser = {
id: string;
type: BypasserType;
};
export type TGetSecretApprovalPoliciesDTO = {
workspaceId: string;
};
@@ -41,6 +51,7 @@ export type TCreateSecretPolicyDTO = {
environment: string;
secretPath?: string | null;
approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number;
enforcementLevel: EnforcementLevel;
allowedSelfApprovals: boolean;
@@ -50,6 +61,7 @@ export type TUpdateSecretPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string | null;
approvals?: number;
allowedSelfApprovals?: boolean;
@@ -57,7 +57,7 @@ export type TSecretApprovalRequest = {
secretPath: string;
hasMerged: boolean;
status: "open" | "close";
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
approvers: {
userId: string;
email: string;
@@ -65,6 +65,13 @@ export type TSecretApprovalRequest = {
lastName: string;
username: string;
}[];
bypassers: {
userId: string;
email: string;
firstName: string;
lastName: string;
username: string;
}[];
};
statusChangedByUserId: string;
statusChangedByUser?: {
@@ -12,7 +12,6 @@ import {
} from "@app/context";
import {
PermissionConditionOperators,
ProjectPermissionApprovalActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
@@ -54,12 +53,10 @@ const SecretPolicyActionSchema = z.object({
});
const ApprovalPolicyActionSchema = z.object({
[ProjectPermissionApprovalActions.Read]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Edit]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Delete]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Create]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowAccessBypass]: z.boolean().optional()
[ProjectPermissionActions.Read]: z.boolean().optional(),
[ProjectPermissionActions.Edit]: z.boolean().optional(),
[ProjectPermissionActions.Delete]: z.boolean().optional(),
[ProjectPermissionActions.Create]: z.boolean().optional()
});
const CmekPolicyActionSchema = z.object({
@@ -574,24 +571,18 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
}
if (subject === ProjectPermissionSub.SecretApproval) {
const canCreate = action.includes(ProjectPermissionApprovalActions.Create);
const canDelete = action.includes(ProjectPermissionApprovalActions.Delete);
const canEdit = action.includes(ProjectPermissionApprovalActions.Edit);
const canRead = action.includes(ProjectPermissionApprovalActions.Read);
const canChangeBypass = action.includes(ProjectPermissionApprovalActions.AllowChangeBypass);
const canAccessBypass = action.includes(ProjectPermissionApprovalActions.AllowAccessBypass);
const canCreate = action.includes(ProjectPermissionActions.Create);
const canDelete = action.includes(ProjectPermissionActions.Delete);
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canRead = action.includes(ProjectPermissionActions.Read);
if (!formVal[subject]) formVal[subject] = [{}];
// Map actions to the keys defined in ApprovalPolicyActionSchema
if (canCreate) formVal[subject]![0][ProjectPermissionApprovalActions.Create] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionApprovalActions.Delete] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionApprovalActions.Edit] = true;
if (canRead) formVal[subject]![0][ProjectPermissionApprovalActions.Read] = true;
if (canChangeBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowChangeBypass] = true;
if (canAccessBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowAccessBypass] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionActions.Create] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionActions.Delete] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionActions.Edit] = true;
if (canRead) formVal[subject]![0][ProjectPermissionActions.Read] = true;
return;
}
@@ -1212,12 +1203,10 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.SecretApproval]: {
title: "Secret Approval Policies",
actions: [
{ label: "Read", value: ProjectPermissionApprovalActions.Read },
{ label: "Create", value: ProjectPermissionApprovalActions.Create },
{ label: "Modify", value: ProjectPermissionApprovalActions.Edit },
{ label: "Remove", value: ProjectPermissionApprovalActions.Delete },
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass },
{ label: "Allow Access Bypass", value: ProjectPermissionApprovalActions.AllowAccessBypass }
{ label: "Read", value: ProjectPermissionActions.Read },
{ label: "Create", value: ProjectPermissionActions.Create },
{ label: "Modify", value: ProjectPermissionActions.Edit },
{ label: "Remove", value: ProjectPermissionActions.Delete }
]
},
[ProjectPermissionSub.SecretRotation]: {
@@ -1694,7 +1683,7 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
},
{
subject: ProjectPermissionSub.SecretApproval,
actions: Object.values(ProjectPermissionApprovalActions)
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.ServiceTokens,
@@ -25,7 +25,6 @@ import {
} from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
import {
ProjectPermissionApprovalActions,
ProjectPermissionMemberActions,
ProjectPermissionSub,
useProjectPermission,
@@ -102,11 +101,6 @@ export const AccessApprovalRequest = ({
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const canBypassApprovalPermission = permission.can(
ProjectPermissionApprovalActions.AllowAccessBypass,
ProjectPermissionSub.SecretApproval
);
const { data: members } = useGetWorkspaceUsers(projectId, true);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(prev, curr) => ({ ...prev, [curr.user.id]: curr }),
@@ -163,6 +157,8 @@ export const AccessApprovalRequest = ({
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
const canBypass =
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
label: "",
@@ -198,6 +194,7 @@ export const AccessApprovalRequest = ({
userReviewStatus,
isAccepted,
isSoftEnforcement,
canBypass,
isRequestedByCurrentUser,
isSelfApproveAllowed
};
@@ -215,9 +212,7 @@ export const AccessApprovalRequest = ({
// Whether the current user can bypass policy
const canBypass =
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
canBypassApprovalPermission;
details.isSoftEnforcement && details.isRequestedByCurrentUser && details.canBypass;
// Whether the current user can approve
const canApprove =
@@ -240,14 +235,7 @@ export const AccessApprovalRequest = ({
handlePopUpOpen("reviewRequest");
},
[
generateRequestDetails,
canBypassApprovalPermission,
membersGroupById,
user,
setSelectedRequest,
handlePopUpOpen
]
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
);
return (
@@ -471,7 +459,7 @@ export const AccessApprovalRequest = ({
setSelectedRequest(null);
refetchRequests();
}}
canBypassApprovalPermission={canBypassApprovalPermission}
canBypass={generateRequestDetails(selectedRequest).canBypass}
/>
)}
@@ -19,7 +19,7 @@ export const ReviewAccessRequestModal = ({
projectSlug,
selectedRequester,
selectedEnvSlug,
canBypassApprovalPermission
canBypass
}: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
@@ -32,7 +32,7 @@ export const ReviewAccessRequestModal = ({
projectSlug: string;
selectedRequester: string | undefined;
selectedEnvSlug: string | undefined;
canBypassApprovalPermission: boolean;
canBypass: boolean;
}) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
const [bypassApproval, setBypassApproval] = useState(false);
@@ -208,7 +208,7 @@ export const ReviewAccessRequestModal = ({
{isSoftEnforcement &&
request.isRequestedByCurrentUser &&
!(request.isApprover && request.isSelfApproveAllowed) &&
canBypassApprovalPermission && (
canBypass && (
<div className="mt-2 flex flex-col space-y-2">
<Checkbox
onCheckedChange={(checked) => setBypassApproval(checked === true)}
@@ -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) => (
@@ -13,11 +13,6 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
import {
ProjectPermissionApprovalActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import {
usePerformSecretApprovalRequestMerge,
useUpdateSecretApprovalRequestStatus
@@ -31,6 +26,7 @@ type Props = {
status: "close" | "open";
approvals: number;
canApprove?: boolean;
isBypasser: boolean;
statusChangeByEmail?: string;
workspaceId: string;
enforcementLevel: EnforcementLevel;
@@ -45,7 +41,8 @@ export const SecretApprovalRequestAction = ({
statusChangeByEmail,
workspaceId,
enforcementLevel,
canApprove
canApprove,
isBypasser
}: Props) => {
const { mutateAsync: performSecretApprovalMerge, isPending: isMerging } =
usePerformSecretApprovalRequestMerge();
@@ -53,12 +50,6 @@ export const SecretApprovalRequestAction = ({
const { mutateAsync: updateSecretStatusChange, isPending: isStatusChanging } =
useUpdateSecretApprovalRequestStatus();
const { permission } = useProjectPermission();
const canBypassApprovalPermission = permission.can(
ProjectPermissionApprovalActions.AllowChangeBypass,
ProjectPermissionSub.SecretApproval
);
const [byPassApproval, setByPassApproval] = useState(false);
const [bypassReason, setBypassReason] = useState("");
@@ -134,10 +125,10 @@ export const SecretApprovalRequestAction = ({
)}
</span>
</div>
<div
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
>
{isSoftEnforcement && !isMergable && canBypassApprovalPermission && (
{isSoftEnforcement && !isMergable && isBypasser && (
<div
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
>
<div className="mt-2 flex flex-col space-y-2 pt-2">
<Checkbox
onCheckedChange={(checked) => setByPassApproval(checked === true)}
@@ -169,8 +160,8 @@ export const SecretApprovalRequestAction = ({
</FormControl>
)}
</div>
)}
</div>
</div>
)}
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
{canApprove || isSoftEnforcement ? (
<div className="flex items-center space-x-4">
@@ -139,6 +139,11 @@ export const SecretApprovalRequestChanges = ({
({ userId }) => userId === userSession.id
);
const isBypasser =
!secretApprovalRequestDetails?.policy?.bypassers ||
!secretApprovalRequestDetails.policy.bypassers.length ||
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
Record<string, { status: ApprovalStatus; comment: string }>
>(
@@ -414,6 +419,7 @@ export const SecretApprovalRequestChanges = ({
<div className="mt-2 flex items-center space-x-6 rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<SecretApprovalRequestAction
canApprove={canApprove}
isBypasser={isBypasser === undefined ? true : isBypasser}
approvalRequestId={secretApprovalRequestDetails.id}
hasMerged={hasMerged}
approvals={secretApprovalRequestDetails.policy.approvals || 0}