mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
access request bypass
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
+27
-18
@@ -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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
+89
-37
@@ -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 (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
@@ -115,9 +142,15 @@ 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>
|
||||
|
||||
@@ -149,7 +182,7 @@ export const ReviewAccessRequestModal = ({
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
!!isLoading || (!request.isApprover && !byPassApproval && isSoftEnforcement)
|
||||
!!isLoading || (!request.isApprover && !bypassApproval && isSoftEnforcement)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
@@ -168,19 +201,38 @@ export const ReviewAccessRequestModal = ({
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => 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" : ""
|
||||
)}
|
||||
>
|
||||
<span className="text-sm text-red">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user