From e5229a5377ec4621ad52d745a4cd11f0c801fb57 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 21 May 2025 15:01:54 -0400 Subject: [PATCH] access request bypass --- .../v1/access-approval-request-router.ts | 8 +- .../access-approval-request-service.ts | 164 ++++++++++++------ .../access-approval-request-types.ts | 2 + .../src/ee/services/license/license-fns.ts | 54 +++--- .../SecretApprovalRequestBypassedTemplate.tsx | 9 +- .../src/hooks/api/accessApproval/mutation.tsx | 18 +- .../src/hooks/api/accessApproval/queries.tsx | 13 +- .../AccessApprovalRequest.tsx | 45 +++-- .../components/ReviewAccessModal.tsx | 126 ++++++++++---- 9 files changed, 289 insertions(+), 150 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index b0914d5c4..b55c611ba 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -154,7 +154,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv requestId: z.string().trim() }), body: z.object({ - status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]), + envName: z.string().optional(), // For logging + bypassReason: z.string().optional() }), response: { 200: z.object({ @@ -170,7 +172,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, requestId: req.params.requestId, - status: req.body.status + status: req.body.status, + envName: req.body.envName, + bypassReason: req.body.bypassReason }); return { review }; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 2b2758b2e..6215937c2 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -6,6 +6,7 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { EnforcementLevel } from "@app/lib/types"; import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -323,7 +324,9 @@ export const accessApprovalRequestServiceFactory = ({ status, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + envName, + bypassReason }: TReviewAccessRequestDTO) => { const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); if (!accessApprovalRequest) { @@ -336,7 +339,12 @@ export const accessApprovalRequestServiceFactory = ({ message: "The policy associated with this access request has been deleted." }); } - if (!policy.allowedSelfApprovals && actorId === accessApprovalRequest.requestedByUserId) { + + if ( + !policy.allowedSelfApprovals && + actorId === accessApprovalRequest.requestedByUserId && + policy.enforcementLevel !== EnforcementLevel.Soft + ) { throw new BadRequestError({ message: "Failed to review access approval request. Users are not authorized to review their own request." }); @@ -363,6 +371,11 @@ export const accessApprovalRequestServiceFactory = ({ throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" }); } + const project = await projectDAL.findById(accessApprovalRequest.projectId); + if (!project) { + throw new NotFoundError({ message: "The project associated with this access request was not found." }); + } + const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); @@ -376,68 +389,105 @@ export const accessApprovalRequestServiceFactory = ({ }, tx ); - if (!review) { - const newReview = await accessApprovalRequestReviewerDAL.create( - { - status, - requestId: accessApprovalRequest.id, - reviewerUserId: actorId - }, - tx - ); - const allReviews = [...existingReviews, newReview]; + if (review) { + throw new BadRequestError({ message: "You have already reviewed this request" }); + } - const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); + const newReview = await accessApprovalRequestReviewerDAL.create( + { + status, + requestId: accessApprovalRequest.id, + reviewerUserId: actorId + }, + tx + ); - // approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved. - if (approvedReviews.length === policy.approvals) { - if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { - throw new BadRequestError({ message: "Temporary range is required for temporary access" }); - } + const allReviews = [...existingReviews, newReview]; + const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); - let privilegeId: string | null = null; - - if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { - // Permanent access - const privilege = await additionalPrivilegeDAL.create( - { - userId: accessApprovalRequest.requestedByUserId, - projectId: accessApprovalRequest.projectId, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, - permissions: JSON.stringify(accessApprovalRequest.permissions) - }, - tx - ); - privilegeId = privilege.id; - } else { - // Temporary access - const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); - const startTime = new Date(); - - const privilege = await additionalPrivilegeDAL.create( - { - userId: accessApprovalRequest.requestedByUserId, - projectId: accessApprovalRequest.projectId, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, - permissions: JSON.stringify(accessApprovalRequest.permissions), - isTemporary: true, - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: accessApprovalRequest.temporaryRange!, - temporaryAccessStartTime: startTime, - temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) - }, - tx - ); - privilegeId = privilege.id; - } - - await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx); + if (status === ApprovalStatus.APPROVED && approvedReviews.length >= policy.approvals) { + if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { + throw new BadRequestError({ message: "Temporary range is required for temporary access" }); } - return newReview; + let privilegeId: string | null = null; + + if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { + // Permanent access + const privilege = await additionalPrivilegeDAL.create( + { + userId: accessApprovalRequest.requestedByUserId, + projectId: accessApprovalRequest.projectId, + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + permissions: JSON.stringify(accessApprovalRequest.permissions) + }, + tx + ); + privilegeId = privilege.id; + } else { + // Temporary access + const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); + const startTime = new Date(); + + const privilege = await additionalPrivilegeDAL.create( + { + userId: accessApprovalRequest.requestedByUserId, + projectId: accessApprovalRequest.projectId, + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + permissions: JSON.stringify(accessApprovalRequest.permissions), + isTemporary: true, // Explicitly set to true for the privilege + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: accessApprovalRequest.temporaryRange!, + temporaryAccessStartTime: startTime, + temporaryAccessEndTime: new Date(startTime.getTime() + relativeTempAllocatedTimeInMs) + }, + tx + ); + privilegeId = privilege.id; + } + await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx); } - throw new BadRequestError({ message: "You have already reviewed this request" }); + + const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft; + const wasSelfRequestAndReview = actorId === accessApprovalRequest.requestedByUserId; + + if (isSoftEnforcement && wasSelfRequestAndReview && status === ApprovalStatus.APPROVED) { + const cfg = getConfig(); + const actingUser = await userDAL.findById(actorId, tx); + + if (actingUser) { + const policyApproverUserIds = policy.approvers + .map((ap) => ap.userId) + .filter((id): id is string => typeof id === "string"); + + if (policyApproverUserIds.length > 0) { + const approverUsersForEmail = await userDAL.find({ $in: { id: policyApproverUserIds } }, { tx }); + const recipientEmails = approverUsersForEmail + .map((appUser) => appUser.email) + .filter((email): email is string => !!email); + + if (recipientEmails.length > 0) { + await smtpService.sendMail({ + recipients: recipientEmails, + subjectLine: "Infisical Secret Access Policy Bypassed", + substitutions: { + projectName: project.name, + requesterFullName: `${actingUser.firstName} ${actingUser.lastName}`, + requesterEmail: actingUser.email, + bypassReason: bypassReason || "No reason provided", + secretPath: policy.secretPath || "/", + environment: envName || "Unknown", + approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`, + requestType: "access" + }, + template: SmtpTemplates.AccessSecretRequestBypassed + }); + } + } + } + } + return newReview; }); return reviewStatus; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index 51a5e0ca2..162f8b3c6 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -17,6 +17,8 @@ export type TGetAccessRequestCountDTO = { export type TReviewAccessRequestDTO = { requestId: string; status: ApprovalStatus; + envName?: string; + bypassReason?: string; } & Omit; export type TCreateAccessApprovalRequestDTO = { diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 8ef91c6f8..a8807c61a 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -18,44 +18,44 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, - pitRecovery: false, - ipAllowlisting: false, - rbac: false, - githubOrgSync: false, + pitRecovery: true, + ipAllowlisting: true, + rbac: true, + githubOrgSync: true, customRateLimits: false, - customAlerts: false, - secretAccessInsights: false, - auditLogs: false, - auditLogsRetentionDays: 0, - auditLogStreams: false, + customAlerts: true, + secretAccessInsights: true, + auditLogs: true, + auditLogsRetentionDays: 3, + auditLogStreams: true, auditLogStreamLimit: 3, - samlSSO: false, - hsm: false, - oidcSSO: false, - scim: false, - ldap: false, - groups: false, + samlSSO: true, + hsm: true, + oidcSSO: true, + scim: true, + ldap: true, + groups: true, status: null, trial_end: null, has_used_trial: true, - secretApproval: false, - secretRotation: false, - caCrl: false, - instanceUserManagement: false, - externalKms: false, + secretApproval: true, + secretRotation: true, + caCrl: true, + instanceUserManagement: true, + externalKms: true, rateLimits: { readLimit: 60, writeLimit: 200, secretsLimit: 40 }, - pkiEst: false, - enforceMfa: false, - projectTemplates: false, - kmip: false, - gateway: false, - sshHostGroups: false + pkiEst: true, + enforceMfa: true, + projectTemplates: true, + kmip: true, + gateway: true, + sshHostGroups: true }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx b/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx index bad823bd3..a07110aa3 100644 --- a/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx @@ -12,6 +12,7 @@ interface SecretApprovalRequestBypassedTemplateProps environment: string; bypassReason: string; approvalUrl: string; + requestType: "change" | "access"; } export const SecretApprovalRequestBypassedTemplate = ({ @@ -22,7 +23,8 @@ export const SecretApprovalRequestBypassedTemplate = ({ secretPath, environment, bypassReason, - approvalUrl + approvalUrl, + requestType = "change" }: SecretApprovalRequestBypassedTemplateProps) => { return ( {requesterEmail} - ) has merged a secret to {secretPath} in the {environment} environment - without obtaining the required approval. + ) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "} + {secretPath} in the {environment} environment without obtaining the required + approval. The following reason was provided for bypassing the policy: " diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 9fda2377a..9ba895311 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -129,22 +129,32 @@ export const useReviewAccessRequest = () => { requestId: string; status: "approved" | "rejected"; projectSlug: string; + envName?: string; envSlug?: string; requestedBy?: string; + bypassReason?: string; } >({ - mutationFn: async ({ requestId, status }) => { + mutationFn: async ({ requestId, status, envName, bypassReason }) => { const { data } = await apiRequest.post( `/api/v1/access-approvals/requests/${requestId}/review`, { - status + status, + envName, + bypassReason } ); return data; }, - onSuccess: (_, { projectSlug, envSlug, requestedBy }) => { + onSuccess: (_, { projectSlug, envSlug, requestedBy, envName, bypassReason }) => { queryClient.invalidateQueries({ - queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy) + queryKey: accessApprovalKeys.getAccessApprovalRequests( + projectSlug, + envSlug, + requestedBy, + envName, + bypassReason + ) }); queryClient.invalidateQueries({ queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 1aa40c588..0f666af7a 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -19,8 +19,17 @@ 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, + envName?: string, + bypassReason?: string + ) => + [ + { projectSlug, envSlug, requestedBy, envName, bypassReason }, + "access-approvals-requests" + ] as const, getAccessApprovalRequestCount: (projectSlug: string) => [{ projectSlug }, "access-approval-request-count"] as const }; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 9afe4795d..9f56b92ef 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -83,7 +83,7 @@ export const AccessApprovalRequest = ({ }) => { const [selectedRequest, setSelectedRequest] = useState< | (TAccessApprovalRequest & { - user: TWorkspaceUser["user"] | null; + user: { firstName?: string; lastName?: string; email?: string } | null; isRequestedByCurrentUser: boolean; isApprover: boolean; }) @@ -118,7 +118,7 @@ export const AccessApprovalRequest = ({ projectSlug }); - const { data: requests } = useGetAccessApprovalRequests({ + const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({ projectSlug, authorProjectMembershipId: requestedByFilter, envSlug: envFilter @@ -346,22 +346,23 @@ export const AccessApprovalRequest = ({ tabIndex={0} onClick={() => { if ( - ((!details.isApprover || - details.isReviewedByUser || - details.isRejectedByAnyone || - details.isAccepted) && - !( - details.isSoftEnforcement && - details.isRequestedByCurrentUser && - !details.isAccepted - )) || - (request.requestedByUserId === user.id && !details.isSelfApproveAllowed) + details.isAccepted || + details.isReviewedByUser || + details.isRejectedByAnyone || + (!details.isApprover && + !(details.isSoftEnforcement && details.isRequestedByCurrentUser)) ) return; - if (membersGroupById?.[request.requestedByUserId].user) { + + if ( + membersGroupById?.[request.requestedByUserId].user || + details.isRequestedByCurrentUser + ) { setSelectedRequest({ ...request, - user: membersGroupById?.[request.requestedByUserId].user, + user: details.isRequestedByCurrentUser + ? user + : membersGroupById?.[request.requestedByUserId].user!, isRequestedByCurrentUser: details.isRequestedByCurrentUser, isApprover: details.isApprover }); @@ -371,17 +372,24 @@ export const AccessApprovalRequest = ({ }} onKeyDown={(evt) => { if ( - !details.isApprover || details.isAccepted || details.isReviewedByUser || - details.isRejectedByAnyone + details.isRejectedByAnyone || + (!details.isApprover && + !(details.isSoftEnforcement && details.isRequestedByCurrentUser)) ) return; + if (evt.key === "Enter") { - if (membersGroupById?.[request.requestedByUserId].user) { + if ( + membersGroupById?.[request.requestedByUserId].user || + details.isRequestedByCurrentUser + ) { setSelectedRequest({ ...request, - user: membersGroupById?.[request.requestedByUserId].user, + user: details.isRequestedByCurrentUser + ? user + : membersGroupById?.[request.requestedByUserId].user!, isRequestedByCurrentUser: details.isRequestedByCurrentUser, isApprover: details.isApprover }); @@ -453,6 +461,7 @@ export const AccessApprovalRequest = ({ onOpenChange={() => { handlePopUpClose("reviewRequest"); setSelectedRequest(null); + refetchRequests(); }} /> )} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx index 37239d987..d739d8bb6 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -2,13 +2,15 @@ import { useCallback, useMemo, useState } from "react"; import ms from "ms"; 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"; +import { twMerge } from "tailwind-merge"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; export const ReviewAccessRequestModal = ({ isOpen, @@ -21,7 +23,7 @@ export const ReviewAccessRequestModal = ({ isOpen: boolean; onOpenChange: (isOpen: boolean) => void; request: TAccessApprovalRequest & { - user: TWorkspaceUser["user"] | null; + user: { firstName?: string; lastName?: string; email?: string } | null; isRequestedByCurrentUser: boolean; isApprover: boolean; }; @@ -30,7 +32,9 @@ export const ReviewAccessRequestModal = ({ selectedEnvSlug: string | undefined; }) => { 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 +84,54 @@ 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, + envName: accessDetails.env, + 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, + accessDetails.env, + selectedEnvSlug, + selectedRequester, + onOpenChange + ] + ); return ( @@ -115,9 +142,15 @@ export const ReviewAccessRequestModal = ({ >
- - {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) - {" "} + {request.user && + (request.user.firstName || request.user.lastName) && + request.user.email ? ( + + {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) + + ) : ( + A user + )}{" "} is requesting access to the following resource: @@ -149,7 +182,7 @@ export const ReviewAccessRequestModal = ({
+ {isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && ( -
+
setByPassApproval(checked === true)} - isChecked={byPassApproval} + onCheckedChange={(checked) => setBypassApproval(checked === true)} + isChecked={bypassApproval} id="byPassApproval" checkIndicatorBg="text-white" - className={byPassApproval ? "border-red bg-red hover:bg-red-600" : ""} + className={twMerge( + "mr-2", + bypassApproval ? "border-red bg-red hover:bg-red-600" : "" + )} > - + Approve without waiting for requirements to be met (bypass policy protection) + {bypassApproval && ( + + setBypassReason(e.currentTarget.value)} + placeholder="Enter reason for bypass (min 10 chars)" + leftIcon={} + /> + + )}
)}