mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 11:27:47 +00:00
feat: updated ui based on review
This commit is contained in:
@@ -14,6 +14,17 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (!hasApprovalRequiredColumn) t.integer("approvalsRequired").nullable();
|
if (!hasApprovalRequiredColumn) t.integer("approvalsRequired").nullable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set rejected status for all access request that was rejected and still has status pending
|
||||||
|
await knex(TableName.AccessApprovalRequest)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.AccessApprovalRequestReviewer,
|
||||||
|
`${TableName.AccessApprovalRequest}.id`,
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||||
|
)
|
||||||
|
.where(`${TableName.AccessApprovalRequest}.status` as "status", "pending")
|
||||||
|
.where(`${TableName.AccessApprovalRequestReviewer}.status` as "status", "rejected")
|
||||||
|
.update(`${TableName.AccessApprovalRequest}.status` as "status", "rejected");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ type TAccessApprovalPolicyServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
groupDAL: TGroupDALFactory;
|
groupDAL: TGroupDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "find">;
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find" | "resetReviewByPolicyId">;
|
||||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
|
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
||||||
@@ -481,6 +481,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await accessApprovalRequestDAL.resetReviewByPolicyId(doc.id, tx);
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -173,8 +173,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
permissions: doc.privilegePermissions
|
permissions: doc.privilegePermissions
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
isApproved: doc.status === ApprovalStatus.APPROVED
|
||||||
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId || doc.status !== ApprovalStatus.PENDING
|
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
@@ -556,5 +555,27 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount };
|
const resetReviewByPolicyId = async (policyId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
await (tx || db)(TableName.AccessApprovalRequestReviewer)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.AccessApprovalRequest,
|
||||||
|
`${TableName.AccessApprovalRequest}.id`,
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||||
|
)
|
||||||
|
.where(`${TableName.AccessApprovalRequest}.status` as "status", ApprovalStatus.PENDING)
|
||||||
|
.where(`${TableName.AccessApprovalRequest}.policyId` as "policyId", policyId)
|
||||||
|
.del();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "ResetReviewByPolicyId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...accessApprovalRequestOrm,
|
||||||
|
findById,
|
||||||
|
findRequestsWithPrivilegeByPolicyIds,
|
||||||
|
getCount,
|
||||||
|
resetReviewByPolicyId
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -380,9 +380,10 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 (accessApprovalRequest.status !== ApprovalStatus.PENDING) {
|
||||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
throw new BadRequestError({ message: "The request has been closed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewsGroupById = groupBy(
|
const reviewsGroupById = groupBy(
|
||||||
existingReviews.filter((review) => review.status === ApprovalStatus.APPROVED),
|
existingReviews.filter((review) => review.status === ApprovalStatus.APPROVED),
|
||||||
(i) => i.reviewerUserId
|
(i) => i.reviewerUserId
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ This functionality works in the following way:
|
|||||||
A step policy enables a sequential approval workflow in which approvals
|
A step policy enables a sequential approval workflow in which approvals
|
||||||
must follow the designated chain.
|
must follow the designated chain.
|
||||||
</Note>
|
</Note>
|
||||||

|

|
||||||
|
|
||||||
2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it.
|
2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it.
|
||||||

|

|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EnforcementLevel, PolicyType } from "../policies/enums";
|
import { EnforcementLevel, PolicyType } from "../policies/enums";
|
||||||
import { TProjectPermission } from "../roles/types";
|
import { TProjectPermission } from "../roles/types";
|
||||||
|
import { ApprovalStatus } from "../secretApprovalRequest/types";
|
||||||
import { WorkspaceEnv } from "../workspace/types";
|
import { WorkspaceEnv } from "../workspace/types";
|
||||||
|
|
||||||
export type TAccessApprovalPolicy = {
|
export type TAccessApprovalPolicy = {
|
||||||
@@ -75,7 +76,7 @@ export type TAccessApprovalRequest = {
|
|||||||
permissions: TProjectPermission[];
|
permissions: TProjectPermission[];
|
||||||
isApproved: boolean;
|
isApproved: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
status: ApprovalStatus;
|
||||||
policy: {
|
policy: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
+3
-5
@@ -398,11 +398,9 @@ export const AccessApprovalRequest = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{details.isApprover && (
|
<Badge variant={details.displayData.type}>
|
||||||
<Badge variant={details.displayData.type}>
|
{details.displayData.label}
|
||||||
{details.displayData.label}
|
</Badge>
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+54
-21
@@ -212,15 +212,40 @@ export const ReviewAccessRequestModal = ({
|
|||||||
(approverChain?.approvals || 1);
|
(approverChain?.approvals || 1);
|
||||||
|
|
||||||
const hasRejected = reviewers.filter((el) => el.status === ApprovalStatus.REJECTED).length;
|
const hasRejected = reviewers.filter((el) => el.status === ApprovalStatus.REJECTED).length;
|
||||||
|
|
||||||
return { ...approverChain, reviewers, hasApproved, hasRejected };
|
return { ...approverChain, reviewers, hasApproved, hasRejected };
|
||||||
});
|
});
|
||||||
return { approvers, membersGroupById, projectGroupsGroupById };
|
const currentSequenceApprover = approvers?.find((el) => !el.hasApproved);
|
||||||
|
const currentSequence = currentSequenceApprover?.sequence || 1;
|
||||||
|
const isMyReviewInThisSequence = currentSequenceApprover?.reviewers.find(
|
||||||
|
(i) => i.userId === user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
approvers,
|
||||||
|
membersGroupById,
|
||||||
|
projectGroupsGroupById,
|
||||||
|
currentSequence,
|
||||||
|
isMyReviewInThisSequence
|
||||||
|
};
|
||||||
}, [request, policies]);
|
}, [request, policies]);
|
||||||
|
|
||||||
const hasRejected = request.reviewers.find((el) => el.status === ApprovalStatus.REJECTED);
|
const hasRejected = request.status === ApprovalStatus.REJECTED;
|
||||||
|
const hasApproved = request.status === ApprovalStatus.APPROVED;
|
||||||
const isReviewedByMe = request.reviewers.find((i) => i.userId === user.id);
|
const isReviewedByMe = request.reviewers.find((i) => i.userId === user.id);
|
||||||
|
|
||||||
|
const shouldBlockRequestActions =
|
||||||
|
hasRejected ||
|
||||||
|
hasApproved ||
|
||||||
|
isReviewedByMe ||
|
||||||
|
(!approverSequence?.isMyReviewInThisSequence && !canBypass);
|
||||||
|
|
||||||
|
const renderCompletedMessages = () => {
|
||||||
|
if (hasRejected) return "This request has been rejected.";
|
||||||
|
if (hasApproved) return "This request has been approved.";
|
||||||
|
if (isReviewedByMe) return "You have reviewed this request.";
|
||||||
|
return "You are not the reviewer in this step.";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
@@ -270,22 +295,27 @@ export const ReviewAccessRequestModal = ({
|
|||||||
{approverSequence?.approvers?.map((approver, index) => (
|
{approverSequence?.approvers?.map((approver, index) => (
|
||||||
<div
|
<div
|
||||||
key={`approval-list-${index + 1}`}
|
key={`approval-list-${index + 1}`}
|
||||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
|
className={twMerge(
|
||||||
|
"relative mb-2 flex items-center rounded border border-mineshaft-500 bg-mineshaft-700 p-4",
|
||||||
|
approverSequence?.currentSequence !== approver.sequence &&
|
||||||
|
!hasApproved &&
|
||||||
|
"text-mineshaft-400"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mr-8 flex h-8 w-8 items-center justify-center border border-bunker-300 bg-bunker-800 text-white",
|
"mr-8 flex h-8 w-8 items-center justify-center text-3xl font-medium",
|
||||||
approver.hasApproved && "border-green-400 text-green-400",
|
approver.hasApproved && "border-green-400 text-green-400",
|
||||||
approver.hasRejected && "border-red-500 text-red-500"
|
approver.hasRejected && "border-red-500 text-red-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="text-lg">{index + 1}</div>
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
|
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"absolute bottom-0 left-8 h-6 border-r border-gray-400",
|
"absolute bottom-0 left-8 h-5 border-r-2 border-gray-400",
|
||||||
approver.hasApproved && "border-green-400",
|
approver.hasApproved && "border-green-400",
|
||||||
approver.hasRejected && "border-red-500"
|
approver.hasRejected && "border-red-500"
|
||||||
)}
|
)}
|
||||||
@@ -294,7 +324,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"absolute left-8 top-0 h-4 border-r border-gray-400",
|
"absolute left-8 top-0 h-5 border-r-2 border-gray-400",
|
||||||
approver.hasApproved && "border-green-400",
|
approver.hasApproved && "border-green-400",
|
||||||
approver.hasRejected && "border-red-500"
|
approver.hasRejected && "border-red-500"
|
||||||
)}
|
)}
|
||||||
@@ -331,20 +361,17 @@ export const ReviewAccessRequestModal = ({
|
|||||||
<div className="ml-16">
|
<div className="ml-16">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faUsers} />
|
||||||
icon={faUsers}
|
|
||||||
className={twMerge(
|
|
||||||
approver.hasApproved && "border-green-400 text-green-400",
|
|
||||||
approver.hasRejected && "border-red-500 text-red-500"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent hideCloseBtn className="pt-3">
|
<PopoverContent hideCloseBtn className="pt-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||||
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
|
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
|
||||||
{approver.reviewers.map((el, idx) => (
|
{approver.reviewers.map((el, idx) => (
|
||||||
<div key={`reviewer-${idx}`} className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm">
|
<div
|
||||||
|
key={`reviewer-${idx + 1}`}
|
||||||
|
className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm"
|
||||||
|
>
|
||||||
<div className="flex-grow">{el.username}</div>
|
<div className="flex-grow">{el.username}</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
|
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
|
||||||
@@ -363,16 +390,22 @@ export const ReviewAccessRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{hasRejected || isReviewedByMe ? (
|
{approverSequence.isMyReviewInThisSequence &&
|
||||||
|
request.status === ApprovalStatus.PENDING && (
|
||||||
|
<div className="mb-4 rounded-r border-l-2 border-l-primary-400 bg-mineshaft-300/5 px-4 py-2.5 text-sm">
|
||||||
|
Awaiting review from you.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shouldBlockRequestActions ? (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||||
isReviewedByMe && "border-l-green-400"
|
isReviewedByMe && "border-l-green-400",
|
||||||
|
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
|
||||||
|
hasRejected && "border-l-red-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isReviewedByMe
|
{renderCompletedMessages()}
|
||||||
? "You have reviewed this request."
|
|
||||||
: "This request has been rejected."}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user