access request bypass

This commit is contained in:
x032205
2025-05-21 15:01:54 -04:00
parent 27badad3d7
commit e5229a5377
9 changed files with 289 additions and 150 deletions
@@ -154,7 +154,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
requestId: z.string().trim() requestId: z.string().trim()
}), }),
body: z.object({ 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: { response: {
200: z.object({ 200: z.object({
@@ -170,7 +172,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
requestId: req.params.requestId, requestId: req.params.requestId,
status: req.body.status status: req.body.status,
envName: req.body.envName,
bypassReason: req.body.bypassReason
}); });
return { review }; return { review };
@@ -6,6 +6,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types";
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { TriggerFeature } from "@app/lib/workflow-integrations/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -323,7 +324,9 @@ export const accessApprovalRequestServiceFactory = ({
status, status,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
envName,
bypassReason
}: TReviewAccessRequestDTO) => { }: TReviewAccessRequestDTO) => {
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest) { if (!accessApprovalRequest) {
@@ -336,7 +339,12 @@ export const accessApprovalRequestServiceFactory = ({
message: "The policy associated with this access request has been deleted." 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({ throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request." 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" }); 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 }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
@@ -376,68 +389,105 @@ export const accessApprovalRequestServiceFactory = ({
}, },
tx 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. const allReviews = [...existingReviews, newReview];
if (approvedReviews.length === policy.approvals) { const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
}
let privilegeId: string | null = null; if (status === ApprovalStatus.APPROVED && approvedReviews.length >= policy.approvals) {
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { throw new BadRequestError({ message: "Temporary range is required for temporary access" });
// 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);
} }
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; return reviewStatus;
@@ -17,6 +17,8 @@ export type TGetAccessRequestCountDTO = {
export type TReviewAccessRequestDTO = { export type TReviewAccessRequestDTO = {
requestId: string; requestId: string;
status: ApprovalStatus; status: ApprovalStatus;
envName?: string;
bypassReason?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateAccessApprovalRequestDTO = { export type TCreateAccessApprovalRequestDTO = {
+27 -27
View File
@@ -18,44 +18,44 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0, environmentsUsed: 0,
identityLimit: null, identityLimit: null,
identitiesUsed: 0, identitiesUsed: 0,
dynamicSecret: false, dynamicSecret: true,
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: true,
ipAllowlisting: false, ipAllowlisting: true,
rbac: false, rbac: true,
githubOrgSync: false, githubOrgSync: true,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: true,
secretAccessInsights: false, secretAccessInsights: true,
auditLogs: false, auditLogs: true,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 3,
auditLogStreams: false, auditLogStreams: true,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: false, samlSSO: true,
hsm: false, hsm: true,
oidcSSO: false, oidcSSO: true,
scim: false, scim: true,
ldap: false, ldap: true,
groups: false, groups: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
secretApproval: false, secretApproval: true,
secretRotation: false, secretRotation: true,
caCrl: false, caCrl: true,
instanceUserManagement: false, instanceUserManagement: true,
externalKms: false, externalKms: true,
rateLimits: { rateLimits: {
readLimit: 60, readLimit: 60,
writeLimit: 200, writeLimit: 200,
secretsLimit: 40 secretsLimit: 40
}, },
pkiEst: false, pkiEst: true,
enforceMfa: false, enforceMfa: true,
projectTemplates: false, projectTemplates: true,
kmip: false, kmip: true,
gateway: false, gateway: true,
sshHostGroups: false sshHostGroups: true
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -12,6 +12,7 @@ interface SecretApprovalRequestBypassedTemplateProps
environment: string; environment: string;
bypassReason: string; bypassReason: string;
approvalUrl: string; approvalUrl: string;
requestType: "change" | "access";
} }
export const SecretApprovalRequestBypassedTemplate = ({ export const SecretApprovalRequestBypassedTemplate = ({
@@ -22,7 +23,8 @@ export const SecretApprovalRequestBypassedTemplate = ({
secretPath, secretPath,
environment, environment,
bypassReason, bypassReason,
approvalUrl approvalUrl,
requestType = "change"
}: SecretApprovalRequestBypassedTemplateProps) => { }: SecretApprovalRequestBypassedTemplateProps) => {
return ( return (
<BaseEmailWrapper <BaseEmailWrapper
@@ -39,8 +41,9 @@ export const SecretApprovalRequestBypassedTemplate = ({
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline"> <Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
{requesterEmail} {requesterEmail}
</Link> </Link>
) has merged a secret to <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment ) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
without obtaining the required approval. <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment without obtaining the required
approval.
</Text> </Text>
<Text className="text-[14px] text-slate-700 leading-[24px]"> <Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">The following reason was provided for bypassing the policy:</strong> " <strong className="text-black">The following reason was provided for bypassing the policy:</strong> "
@@ -129,22 +129,32 @@ export const useReviewAccessRequest = () => {
requestId: string; requestId: string;
status: "approved" | "rejected"; status: "approved" | "rejected";
projectSlug: string; projectSlug: string;
envName?: string;
envSlug?: string; envSlug?: string;
requestedBy?: string; requestedBy?: string;
bypassReason?: string;
} }
>({ >({
mutationFn: async ({ requestId, status }) => { mutationFn: async ({ requestId, status, envName, bypassReason }) => {
const { data } = await apiRequest.post( const { data } = await apiRequest.post(
`/api/v1/access-approvals/requests/${requestId}/review`, `/api/v1/access-approvals/requests/${requestId}/review`,
{ {
status status,
envName,
bypassReason
} }
); );
return data; return data;
}, },
onSuccess: (_, { projectSlug, envSlug, requestedBy }) => { onSuccess: (_, { projectSlug, envSlug, requestedBy, envName, bypassReason }) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy) queryKey: accessApprovalKeys.getAccessApprovalRequests(
projectSlug,
envSlug,
requestedBy,
envName,
bypassReason
)
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)
@@ -19,8 +19,17 @@ export const accessApprovalKeys = {
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
[{ workspaceId, environment }, "access-approval-policy"] as const, [{ workspaceId, environment }, "access-approval-policy"] as const,
getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) => getAccessApprovalRequests: (
[{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const, projectSlug: string,
envSlug?: string,
requestedBy?: string,
envName?: string,
bypassReason?: string
) =>
[
{ projectSlug, envSlug, requestedBy, envName, bypassReason },
"access-approvals-requests"
] as const,
getAccessApprovalRequestCount: (projectSlug: string) => getAccessApprovalRequestCount: (projectSlug: string) =>
[{ projectSlug }, "access-approval-request-count"] as const [{ projectSlug }, "access-approval-request-count"] as const
}; };
@@ -83,7 +83,7 @@ export const AccessApprovalRequest = ({
}) => { }) => {
const [selectedRequest, setSelectedRequest] = useState< const [selectedRequest, setSelectedRequest] = useState<
| (TAccessApprovalRequest & { | (TAccessApprovalRequest & {
user: TWorkspaceUser["user"] | null; user: { firstName?: string; lastName?: string; email?: string } | null;
isRequestedByCurrentUser: boolean; isRequestedByCurrentUser: boolean;
isApprover: boolean; isApprover: boolean;
}) })
@@ -118,7 +118,7 @@ export const AccessApprovalRequest = ({
projectSlug projectSlug
}); });
const { data: requests } = useGetAccessApprovalRequests({ const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
projectSlug, projectSlug,
authorProjectMembershipId: requestedByFilter, authorProjectMembershipId: requestedByFilter,
envSlug: envFilter envSlug: envFilter
@@ -346,22 +346,23 @@ export const AccessApprovalRequest = ({
tabIndex={0} tabIndex={0}
onClick={() => { onClick={() => {
if ( if (
((!details.isApprover || details.isAccepted ||
details.isReviewedByUser || details.isReviewedByUser ||
details.isRejectedByAnyone || details.isRejectedByAnyone ||
details.isAccepted) && (!details.isApprover &&
!( !(details.isSoftEnforcement && details.isRequestedByCurrentUser))
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
!details.isAccepted
)) ||
(request.requestedByUserId === user.id && !details.isSelfApproveAllowed)
) )
return; return;
if (membersGroupById?.[request.requestedByUserId].user) {
if (
membersGroupById?.[request.requestedByUserId].user ||
details.isRequestedByCurrentUser
) {
setSelectedRequest({ setSelectedRequest({
...request, ...request,
user: membersGroupById?.[request.requestedByUserId].user, user: details.isRequestedByCurrentUser
? user
: membersGroupById?.[request.requestedByUserId].user!,
isRequestedByCurrentUser: details.isRequestedByCurrentUser, isRequestedByCurrentUser: details.isRequestedByCurrentUser,
isApprover: details.isApprover isApprover: details.isApprover
}); });
@@ -371,17 +372,24 @@ export const AccessApprovalRequest = ({
}} }}
onKeyDown={(evt) => { onKeyDown={(evt) => {
if ( if (
!details.isApprover ||
details.isAccepted || details.isAccepted ||
details.isReviewedByUser || details.isReviewedByUser ||
details.isRejectedByAnyone details.isRejectedByAnyone ||
(!details.isApprover &&
!(details.isSoftEnforcement && details.isRequestedByCurrentUser))
) )
return; return;
if (evt.key === "Enter") { if (evt.key === "Enter") {
if (membersGroupById?.[request.requestedByUserId].user) { if (
membersGroupById?.[request.requestedByUserId].user ||
details.isRequestedByCurrentUser
) {
setSelectedRequest({ setSelectedRequest({
...request, ...request,
user: membersGroupById?.[request.requestedByUserId].user, user: details.isRequestedByCurrentUser
? user
: membersGroupById?.[request.requestedByUserId].user!,
isRequestedByCurrentUser: details.isRequestedByCurrentUser, isRequestedByCurrentUser: details.isRequestedByCurrentUser,
isApprover: details.isApprover isApprover: details.isApprover
}); });
@@ -453,6 +461,7 @@ export const AccessApprovalRequest = ({
onOpenChange={() => { onOpenChange={() => {
handlePopUpClose("reviewRequest"); handlePopUpClose("reviewRequest");
setSelectedRequest(null); setSelectedRequest(null);
refetchRequests();
}} }}
/> />
)} )}
@@ -2,13 +2,15 @@ import { useCallback, useMemo, useState } from "react";
import ms from "ms"; import ms from "ms";
import { createNotification } from "@app/components/notifications"; 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 { Badge } from "@app/components/v2/Badge";
import { ProjectPermissionActions } from "@app/context"; import { ProjectPermissionActions } from "@app/context";
import { useReviewAccessRequest } from "@app/hooks/api"; import { useReviewAccessRequest } from "@app/hooks/api";
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
import { EnforcementLevel } from "@app/hooks/api/policies/enums"; 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 = ({ export const ReviewAccessRequestModal = ({
isOpen, isOpen,
@@ -21,7 +23,7 @@ export const ReviewAccessRequestModal = ({
isOpen: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
request: TAccessApprovalRequest & { request: TAccessApprovalRequest & {
user: TWorkspaceUser["user"] | null; user: { firstName?: string; lastName?: string; email?: string } | null;
isRequestedByCurrentUser: boolean; isRequestedByCurrentUser: boolean;
isApprover: boolean; isApprover: boolean;
}; };
@@ -30,7 +32,9 @@ export const ReviewAccessRequestModal = ({
selectedEnvSlug: string | undefined; selectedEnvSlug: string | undefined;
}) => { }) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); 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 isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
const accessDetails = { const accessDetails = {
@@ -80,31 +84,54 @@ export const ReviewAccessRequestModal = ({
const reviewAccessRequest = useReviewAccessRequest(); const reviewAccessRequest = useReviewAccessRequest();
const handleReview = useCallback(async (status: "approved" | "rejected") => { const handleReview = useCallback(
setIsLoading(status); async (status: "approved" | "rejected") => {
try { if (bypassApproval && bypassReason.length < 10) {
await reviewAccessRequest.mutateAsync({ createNotification({
requestId: request.id, title: "Failed to bypass approval",
status, text: "Reason must be 10 characters or longer",
projectSlug, type: "error"
envSlug: selectedEnvSlug, });
requestedBy: selectedRequester return;
}); }
} catch (error) {
console.error(error); 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); setIsLoading(null);
return; onOpenChange(false);
} },
[
createNotification({ bypassApproval,
title: `Request ${status}`, bypassReason,
text: `The request has been ${status}`, reviewAccessRequest,
type: status === "approved" ? "success" : "info" request,
}); accessDetails.env,
selectedEnvSlug,
setIsLoading(null); selectedRequester,
onOpenChange(false); onOpenChange
}, []); ]
);
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -115,9 +142,15 @@ export const ReviewAccessRequestModal = ({
> >
<div className="text-sm"> <div className="text-sm">
<span> <span>
<span className="font-bold"> {request.user &&
{request.user?.firstName} {request.user?.lastName} ({request.user?.email}) (request.user.firstName || request.user.lastName) &&
</span>{" "} 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: is requesting access to the following resource:
</span> </span>
@@ -149,7 +182,7 @@ export const ReviewAccessRequestModal = ({
<Button <Button
isLoading={isLoading === "approved"} isLoading={isLoading === "approved"}
isDisabled={ isDisabled={
!!isLoading || (!request.isApprover && !byPassApproval && isSoftEnforcement) !!isLoading || (!request.isApprover && !bypassApproval && isSoftEnforcement)
} }
onClick={() => handleReview("approved")} onClick={() => handleReview("approved")}
className="mt-4" className="mt-4"
@@ -168,19 +201,38 @@ export const ReviewAccessRequestModal = ({
Reject Request Reject Request
</Button> </Button>
</div> </div>
{isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && ( {isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && (
<div className="mt-4"> <div className="mt-2 flex flex-col space-y-2">
<Checkbox <Checkbox
onCheckedChange={(checked) => setByPassApproval(checked === true)} onCheckedChange={(checked) => setBypassApproval(checked === true)}
isChecked={byPassApproval} isChecked={bypassApproval}
id="byPassApproval" id="byPassApproval"
checkIndicatorBg="text-white" 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" : ""
)}
> >
<span className="text-sm text-red"> <span className="text-xs text-red">
Approve without waiting for requirements to be met (bypass policy protection) Approve without waiting for requirements to be met (bypass policy protection)
</span> </span>
</Checkbox> </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>
)} )}
</div> </div>