mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
Merge pull request #3643 from Infisical/ENG-2801
feat(policies): Approval Request Break-Glass Bypass
This commit is contained in:
@@ -29,7 +29,8 @@ export enum ProjectPermissionApprovalActions {
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
AllowChangeBypass = "allow-change-bypass"
|
||||
AllowChangeBypass = "allow-change-bypass",
|
||||
AllowAccessBypass = "allow-access-bypass"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
|
||||
@@ -131,20 +131,27 @@ export const useReviewAccessRequest = () => {
|
||||
projectSlug: string;
|
||||
envSlug?: string;
|
||||
requestedBy?: string;
|
||||
bypassReason?: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ requestId, status }) => {
|
||||
mutationFn: async ({ requestId, status, bypassReason }) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/access-approvals/requests/${requestId}/review`,
|
||||
{
|
||||
status
|
||||
status,
|
||||
bypassReason
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug, envSlug, requestedBy }) => {
|
||||
onSuccess: (_, { projectSlug, envSlug, requestedBy, bypassReason }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy)
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
||||
projectSlug,
|
||||
envSlug,
|
||||
requestedBy,
|
||||
bypassReason
|
||||
)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)
|
||||
|
||||
@@ -19,8 +19,12 @@ export const accessApprovalKeys = {
|
||||
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
|
||||
[{ workspaceId, environment }, "access-approval-policy"] as const,
|
||||
|
||||
getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) =>
|
||||
[{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const,
|
||||
getAccessApprovalRequests: (
|
||||
projectSlug: string,
|
||||
envSlug?: string,
|
||||
requestedBy?: string,
|
||||
bypassReason?: string
|
||||
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
|
||||
getAccessApprovalRequestCount: (projectSlug: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count"] as const
|
||||
};
|
||||
|
||||
+7
-2
@@ -58,7 +58,8 @@ const ApprovalPolicyActionSchema = z.object({
|
||||
[ProjectPermissionApprovalActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionApprovalActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionApprovalActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional()
|
||||
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional(),
|
||||
[ProjectPermissionApprovalActions.AllowAccessBypass]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const CmekPolicyActionSchema = z.object({
|
||||
@@ -578,6 +579,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const canEdit = action.includes(ProjectPermissionApprovalActions.Edit);
|
||||
const canRead = action.includes(ProjectPermissionApprovalActions.Read);
|
||||
const canChangeBypass = action.includes(ProjectPermissionApprovalActions.AllowChangeBypass);
|
||||
const canAccessBypass = action.includes(ProjectPermissionApprovalActions.AllowAccessBypass);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
@@ -588,6 +590,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (canRead) formVal[subject]![0][ProjectPermissionApprovalActions.Read] = true;
|
||||
if (canChangeBypass)
|
||||
formVal[subject]![0][ProjectPermissionApprovalActions.AllowChangeBypass] = true;
|
||||
if (canAccessBypass)
|
||||
formVal[subject]![0][ProjectPermissionApprovalActions.AllowAccessBypass] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1212,7 +1216,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Create", value: ProjectPermissionApprovalActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionApprovalActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionApprovalActions.Delete },
|
||||
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass }
|
||||
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass },
|
||||
{ label: "Allow Access Bypass", value: ProjectPermissionApprovalActions.AllowAccessBypass }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretRotation]: {
|
||||
|
||||
+109
-91
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import {
|
||||
ProjectPermissionApprovalActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
@@ -83,8 +84,9 @@ export const AccessApprovalRequest = ({
|
||||
}) => {
|
||||
const [selectedRequest, setSelectedRequest] = useState<
|
||||
| (TAccessApprovalRequest & {
|
||||
user: TWorkspaceUser["user"] | null;
|
||||
user: { firstName?: string; lastName?: string; email?: string } | null;
|
||||
isRequestedByCurrentUser: boolean;
|
||||
isSelfApproveAllowed: boolean;
|
||||
isApprover: boolean;
|
||||
})
|
||||
| null
|
||||
@@ -100,6 +102,11 @@ 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 }),
|
||||
@@ -118,7 +125,7 @@ export const AccessApprovalRequest = ({
|
||||
projectSlug
|
||||
});
|
||||
|
||||
const { data: requests } = useGetAccessApprovalRequests({
|
||||
const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
|
||||
projectSlug,
|
||||
authorProjectMembershipId: requestedByFilter,
|
||||
envSlug: envFilter
|
||||
@@ -143,56 +150,105 @@ export const AccessApprovalRequest = ({
|
||||
return requests;
|
||||
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
||||
|
||||
const generateRequestDetails = (request: TAccessApprovalRequest) => {
|
||||
const isReviewedByUser = request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
||||
const isRejectedByAnyone = request.reviewers.some(
|
||||
({ status }) => status === ApprovalStatus.REJECTED
|
||||
);
|
||||
const isApprover = request.policy.approvers.indexOf(user.id || "") !== -1;
|
||||
const isAccepted = request.isApproved;
|
||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
|
||||
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
|
||||
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
||||
const generateRequestDetails = useCallback(
|
||||
(request: TAccessApprovalRequest) => {
|
||||
const isReviewedByUser =
|
||||
request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
||||
const isRejectedByAnyone = request.reviewers.some(
|
||||
({ status }) => status === ApprovalStatus.REJECTED
|
||||
);
|
||||
const isApprover = request.policy.approvers.indexOf(user.id || "") !== -1;
|
||||
const isAccepted = request.isApproved;
|
||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
|
||||
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
|
||||
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
||||
|
||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||
label: "",
|
||||
type: "primary"
|
||||
};
|
||||
|
||||
const isExpired =
|
||||
request.privilege &&
|
||||
request.isApproved &&
|
||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
type: "primary"
|
||||
};
|
||||
} else if (!isReviewedByUser)
|
||||
displayData = {
|
||||
label: "Review Required",
|
||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||
label: "",
|
||||
type: "primary"
|
||||
};
|
||||
|
||||
return {
|
||||
displayData,
|
||||
isReviewedByUser,
|
||||
isRejectedByAnyone,
|
||||
isApprover,
|
||||
userReviewStatus,
|
||||
isAccepted,
|
||||
isSoftEnforcement,
|
||||
isRequestedByCurrentUser,
|
||||
isSelfApproveAllowed
|
||||
};
|
||||
};
|
||||
const isExpired =
|
||||
request.privilege &&
|
||||
request.isApproved &&
|
||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
type: "primary"
|
||||
};
|
||||
} else if (!isReviewedByUser)
|
||||
displayData = {
|
||||
label: "Review Required",
|
||||
type: "primary"
|
||||
};
|
||||
|
||||
return {
|
||||
displayData,
|
||||
isReviewedByUser,
|
||||
isRejectedByAnyone,
|
||||
isApprover,
|
||||
userReviewStatus,
|
||||
isAccepted,
|
||||
isSoftEnforcement,
|
||||
isRequestedByCurrentUser,
|
||||
isSelfApproveAllowed
|
||||
};
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const handleSelectRequest = useCallback(
|
||||
(request: TAccessApprovalRequest) => {
|
||||
const details = generateRequestDetails(request);
|
||||
|
||||
// Whether the request has already been approved / rejected / reviewed
|
||||
const isInactive =
|
||||
details.isAccepted || details.isReviewedByUser || details.isRejectedByAnyone;
|
||||
|
||||
// Whether the current user can bypass policy
|
||||
const canBypass =
|
||||
details.isSoftEnforcement &&
|
||||
details.isRequestedByCurrentUser &&
|
||||
canBypassApprovalPermission;
|
||||
|
||||
// Whether the current user can approve
|
||||
const canApprove =
|
||||
details.isApprover && (!details.isRequestedByCurrentUser || details.isSelfApproveAllowed);
|
||||
|
||||
if (isInactive || (!canApprove && !canBypass)) return;
|
||||
|
||||
if (membersGroupById?.[request.requestedByUserId].user || details.isRequestedByCurrentUser) {
|
||||
setSelectedRequest({
|
||||
...request,
|
||||
user:
|
||||
details.isRequestedByCurrentUser || !membersGroupById?.[request.requestedByUserId].user
|
||||
? user
|
||||
: membersGroupById?.[request.requestedByUserId].user,
|
||||
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
|
||||
isSelfApproveAllowed: details.isSelfApproveAllowed,
|
||||
isApprover: details.isApprover
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("reviewRequest");
|
||||
},
|
||||
[
|
||||
generateRequestDetails,
|
||||
canBypassApprovalPermission,
|
||||
membersGroupById,
|
||||
user,
|
||||
setSelectedRequest,
|
||||
handlePopUpOpen
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -344,50 +400,10 @@ export const AccessApprovalRequest = ({
|
||||
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (
|
||||
((!details.isApprover ||
|
||||
details.isReviewedByUser ||
|
||||
details.isRejectedByAnyone ||
|
||||
details.isAccepted) &&
|
||||
!(
|
||||
details.isSoftEnforcement &&
|
||||
details.isRequestedByCurrentUser &&
|
||||
!details.isAccepted
|
||||
)) ||
|
||||
(request.requestedByUserId === user.id && !details.isSelfApproveAllowed)
|
||||
)
|
||||
return;
|
||||
if (membersGroupById?.[request.requestedByUserId].user) {
|
||||
setSelectedRequest({
|
||||
...request,
|
||||
user: membersGroupById?.[request.requestedByUserId].user,
|
||||
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
|
||||
isApprover: details.isApprover
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("reviewRequest");
|
||||
}}
|
||||
onClick={() => handleSelectRequest(request)}
|
||||
onKeyDown={(evt) => {
|
||||
if (
|
||||
!details.isApprover ||
|
||||
details.isAccepted ||
|
||||
details.isReviewedByUser ||
|
||||
details.isRejectedByAnyone
|
||||
)
|
||||
return;
|
||||
if (evt.key === "Enter") {
|
||||
if (membersGroupById?.[request.requestedByUserId].user) {
|
||||
setSelectedRequest({
|
||||
...request,
|
||||
user: membersGroupById?.[request.requestedByUserId].user,
|
||||
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
|
||||
isApprover: details.isApprover
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("reviewRequest");
|
||||
handleSelectRequest(request);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -453,7 +469,9 @@ export const AccessApprovalRequest = ({
|
||||
onOpenChange={() => {
|
||||
handlePopUpClose("reviewRequest");
|
||||
setSelectedRequest(null);
|
||||
refetchRequests();
|
||||
}}
|
||||
canBypassApprovalPermission={canBypassApprovalPermission}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+108
-50
@@ -1,14 +1,16 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Checkbox, Modal, ModalContent } from "@app/components/v2";
|
||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import { ProjectPermissionActions } from "@app/context";
|
||||
import { useReviewAccessRequest } from "@app/hooks/api";
|
||||
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
|
||||
import { EnforcementLevel } from "@app/hooks/api/policies/enums";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||
|
||||
export const ReviewAccessRequestModal = ({
|
||||
isOpen,
|
||||
@@ -16,21 +18,26 @@ export const ReviewAccessRequestModal = ({
|
||||
request,
|
||||
projectSlug,
|
||||
selectedRequester,
|
||||
selectedEnvSlug
|
||||
selectedEnvSlug,
|
||||
canBypassApprovalPermission
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
request: TAccessApprovalRequest & {
|
||||
user: TWorkspaceUser["user"] | null;
|
||||
user: { firstName?: string; lastName?: string; email?: string } | null;
|
||||
isRequestedByCurrentUser: boolean;
|
||||
isSelfApproveAllowed: boolean;
|
||||
isApprover: boolean;
|
||||
};
|
||||
projectSlug: string;
|
||||
selectedRequester: string | undefined;
|
||||
selectedEnvSlug: string | undefined;
|
||||
canBypassApprovalPermission: boolean;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||
const [byPassApproval, setByPassApproval] = useState(false);
|
||||
const [bypassApproval, setBypassApproval] = useState(false);
|
||||
const [bypassReason, setBypassReason] = useState("");
|
||||
|
||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
|
||||
const accessDetails = {
|
||||
@@ -80,31 +87,52 @@ export const ReviewAccessRequestModal = ({
|
||||
|
||||
const reviewAccessRequest = useReviewAccessRequest();
|
||||
|
||||
const handleReview = useCallback(async (status: "approved" | "rejected") => {
|
||||
setIsLoading(status);
|
||||
try {
|
||||
await reviewAccessRequest.mutateAsync({
|
||||
requestId: request.id,
|
||||
status,
|
||||
projectSlug,
|
||||
envSlug: selectedEnvSlug,
|
||||
requestedBy: selectedRequester
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const handleReview = useCallback(
|
||||
async (status: "approved" | "rejected") => {
|
||||
if (bypassApproval && bypassReason.length < 10) {
|
||||
createNotification({
|
||||
title: "Failed to bypass approval",
|
||||
text: "Reason must be 10 characters or longer",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(status);
|
||||
try {
|
||||
await reviewAccessRequest.mutateAsync({
|
||||
requestId: request.id,
|
||||
status,
|
||||
projectSlug,
|
||||
envSlug: selectedEnvSlug,
|
||||
requestedBy: selectedRequester,
|
||||
bypassReason: bypassApproval ? bypassReason : undefined
|
||||
});
|
||||
|
||||
createNotification({
|
||||
title: `Request ${status}`,
|
||||
text: `The request has been ${status}`,
|
||||
type: status === "approved" ? "success" : "info"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsLoading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
title: `Request ${status}`,
|
||||
text: `The request has been ${status}`,
|
||||
type: status === "approved" ? "success" : "info"
|
||||
});
|
||||
|
||||
setIsLoading(null);
|
||||
onOpenChange(false);
|
||||
}, []);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[
|
||||
bypassApproval,
|
||||
bypassReason,
|
||||
reviewAccessRequest,
|
||||
request,
|
||||
selectedEnvSlug,
|
||||
selectedRequester,
|
||||
onOpenChange
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
@@ -115,12 +143,17 @@ export const ReviewAccessRequestModal = ({
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span>
|
||||
<span className="font-bold">
|
||||
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||
</span>{" "}
|
||||
{request.user &&
|
||||
(request.user.firstName || request.user.lastName) &&
|
||||
request.user.email ? (
|
||||
<span className="font-bold">
|
||||
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||
</span>
|
||||
) : (
|
||||
<span>A user</span>
|
||||
)}{" "}
|
||||
is requesting access to the following resource:
|
||||
</span>
|
||||
|
||||
<div className="mb-2 mt-4 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
||||
<div className="mb-1 lowercase">
|
||||
<span className="font-bold capitalize">Requested path: </span>
|
||||
@@ -144,12 +177,16 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
!!isLoading || (!request.isApprover && !byPassApproval && isSoftEnforcement)
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
@@ -168,21 +205,42 @@ export const ReviewAccessRequestModal = ({
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
{isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && (
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
||||
isChecked={byPassApproval}
|
||||
id="byPassApproval"
|
||||
checkIndicatorBg="text-white"
|
||||
className={byPassApproval ? "border-red bg-red hover:bg-red-600" : ""}
|
||||
>
|
||||
<span className="text-sm text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy protection)
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
{isSoftEnforcement &&
|
||||
request.isRequestedByCurrentUser &&
|
||||
!(request.isApprover && request.isSelfApproveAllowed) &&
|
||||
canBypassApprovalPermission && (
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
||||
isChecked={bypassApproval}
|
||||
id="byPassApproval"
|
||||
checkIndicatorBg="text-white"
|
||||
className={twMerge(
|
||||
"mr-2",
|
||||
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
||||
)}
|
||||
>
|
||||
<span className="text-xs text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy protection)
|
||||
</span>
|
||||
</Checkbox>
|
||||
{bypassApproval && (
|
||||
<FormControl
|
||||
label="Reason for bypass"
|
||||
className="mt-2"
|
||||
isRequired
|
||||
tooltipText="Enter a reason for bypassing the secret change policy"
|
||||
>
|
||||
<Input
|
||||
value={bypassReason}
|
||||
onChange={(e) => setBypassReason(e.currentTarget.value)}
|
||||
placeholder="Enter reason for bypass (min 10 chars)"
|
||||
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user