mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 10:29:27 +00:00
feat(approvals): visualization of deprovisioned scim users
This commit is contained in:
@@ -35,6 +35,7 @@ export type Approver = {
|
||||
type: ApproverType;
|
||||
sequence?: number;
|
||||
approvalsRequired?: number;
|
||||
isOrgMembershipActive: boolean;
|
||||
};
|
||||
|
||||
export type Bypasser = {
|
||||
@@ -81,6 +82,7 @@ export type TAccessApprovalRequest = {
|
||||
name: string;
|
||||
approvals: number;
|
||||
approvers: {
|
||||
isOrgMembershipActive: boolean;
|
||||
userId: string;
|
||||
sequence?: number;
|
||||
approvalsRequired?: number;
|
||||
@@ -96,6 +98,7 @@ export type TAccessApprovalRequest = {
|
||||
};
|
||||
|
||||
reviewers: {
|
||||
isOrgMembershipActive: boolean;
|
||||
userId: string;
|
||||
status: string;
|
||||
}[];
|
||||
@@ -166,7 +169,7 @@ export type TCreateAccessPolicyDTO = {
|
||||
projectSlug: string;
|
||||
name?: string;
|
||||
environments: string[];
|
||||
approvers?: Approver[];
|
||||
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
secretPath: string;
|
||||
@@ -178,7 +181,7 @@ export type TCreateAccessPolicyDTO = {
|
||||
export type TUpdateAccessPolicyDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
approvers?: Approver[];
|
||||
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||
bypassers?: Bypasser[];
|
||||
secretPath?: string;
|
||||
environments?: string[];
|
||||
|
||||
@@ -20,6 +20,7 @@ export enum ApproverType {
|
||||
}
|
||||
|
||||
export type Approver = {
|
||||
isOrgMembershipActive: boolean;
|
||||
id: string;
|
||||
type: ApproverType;
|
||||
};
|
||||
@@ -49,7 +50,7 @@ export type TCreateSecretPolicyDTO = {
|
||||
name?: string;
|
||||
environments: string[];
|
||||
secretPath: string;
|
||||
approvers?: Approver[];
|
||||
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@@ -59,7 +60,7 @@ export type TCreateSecretPolicyDTO = {
|
||||
export type TUpdateSecretPolicyDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
approvers?: Approver[];
|
||||
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||
bypassers?: Bypasser[];
|
||||
secretPath?: string;
|
||||
approvals?: number;
|
||||
|
||||
@@ -53,6 +53,7 @@ export type TSecretApprovalRequest = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}[];
|
||||
workspace: string;
|
||||
environment: string;
|
||||
@@ -62,6 +63,7 @@ export type TSecretApprovalRequest = {
|
||||
status: "open" | "close";
|
||||
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
|
||||
approvers: {
|
||||
isOrgMembershipActive: boolean;
|
||||
userId: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
|
||||
@@ -83,6 +83,7 @@ export type TProjectMembership = {
|
||||
export type TWorkspaceUser = {
|
||||
id: string;
|
||||
user: {
|
||||
isOrgMembershipActive: boolean;
|
||||
email: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
|
||||
+57
-11
@@ -3,7 +3,10 @@ import {
|
||||
faBan,
|
||||
faCheck,
|
||||
faHourglass,
|
||||
faTriangleExclamation
|
||||
faQuestionCircle,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUserSlash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
@@ -33,7 +36,7 @@ import { EnforcementLevel } from "@app/hooks/api/policies/enums";
|
||||
import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { groupBy } from "@app/lib/fn/array";
|
||||
|
||||
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||
const getReviewedStatusSymbol = (status?: ApprovalStatus, isOrgMembershipActive?: boolean) => {
|
||||
if (status === ApprovalStatus.APPROVED)
|
||||
return (
|
||||
<Badge variant="success" className="flex h-4 items-center justify-center">
|
||||
@@ -46,6 +49,16 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||
<FontAwesomeIcon icon={faBan} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (!isOrgMembershipActive) {
|
||||
return (
|
||||
// Can't do a tooltip here because nested tooltips doesn't work properly as of yet.
|
||||
// TODO(daniel): Fix nested tooltips in the future.
|
||||
<Badge variant="danger" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faUserSlash} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="primary" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faHourglass} size="xs" />
|
||||
@@ -81,6 +94,7 @@ export const ReviewAccessRequestModal = ({
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||
const [bypassApproval, setBypassApproval] = useState(false);
|
||||
|
||||
const [bypassReason, setBypassReason] = useState("");
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || "");
|
||||
@@ -215,7 +229,10 @@ export const ReviewAccessRequestModal = ({
|
||||
const approvers = approversBySequence?.map((approverChain) => {
|
||||
const reviewers = request.policy.approvers
|
||||
.filter((el) => (el.sequence || 1) === approverChain.sequence)
|
||||
.map((el) => ({ ...el, status: reviewesGroupById?.[el.userId]?.[0]?.status }));
|
||||
.map((el) => ({
|
||||
...el,
|
||||
status: reviewesGroupById?.[el.userId]?.[0]?.status
|
||||
}));
|
||||
const hasApproved =
|
||||
reviewers.filter((el) => el.status === "approved").length >=
|
||||
(approverChain?.approvals || 1);
|
||||
@@ -383,12 +400,31 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||
<GenericFieldLabel className="col-span-2" label="Users">
|
||||
{approver?.user
|
||||
?.map(
|
||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||
)
|
||||
.join(", ")}
|
||||
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||
{Boolean(approver.user.length) && (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{approver?.user?.map((el) => {
|
||||
const member = approverSequence?.membersGroupById?.[el.id]?.[0];
|
||||
if (!member) return null;
|
||||
|
||||
return member.user.isOrgMembershipActive ? (
|
||||
<span key={el.id}>{member.user.username}</span>
|
||||
) : (
|
||||
<span className="opacity-40" key={el.id}>
|
||||
{member.user.username}{" "}
|
||||
<span className="text-xs">
|
||||
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
|
||||
<div>
|
||||
(Inactive){" "}
|
||||
<FontAwesomeIcon size="xs" icon={faQuestionCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-2" label="Groups">
|
||||
{approver?.group
|
||||
@@ -413,8 +449,18 @@ export const ReviewAccessRequestModal = ({
|
||||
key={`reviewer-${idx + 1}`}
|
||||
className="flex items-center gap-2 px-2 py-2 text-sm"
|
||||
>
|
||||
<div className="flex-1">{el.username}</div>
|
||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-1",
|
||||
!el.isOrgMembershipActive && "opacity-40"
|
||||
)}
|
||||
>
|
||||
{el.username}
|
||||
</div>
|
||||
{getReviewedStatusSymbol(
|
||||
el?.status as ApprovalStatus,
|
||||
el.isOrgMembershipActive
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+26
-5
@@ -3,6 +3,7 @@ import {
|
||||
faClipboardCheck,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faQuestionCircle,
|
||||
faTrash,
|
||||
faUser,
|
||||
faUserGroup
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
GenericFieldLabel,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
@@ -86,10 +88,9 @@ export const ApprovalPolicyRow = ({
|
||||
return entityInSameSequence?.map((el) => {
|
||||
return {
|
||||
sequence: el.sequence || policy.approvals,
|
||||
userLabels: members
|
||||
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
||||
.map((member) => getMemberLabel(member))
|
||||
.join(", "),
|
||||
|
||||
users: members.filter((member) => el.user.find((i) => i.id === member.user.id)),
|
||||
|
||||
groupLabels: groups
|
||||
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
|
||||
.map(({ group }) => group.name)
|
||||
@@ -212,7 +213,27 @@ export const ApprovalPolicyRow = ({
|
||||
)}
|
||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||
{el.userLabels}
|
||||
{Boolean(el.users.length) && (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{el.users.map((u, i) => {
|
||||
return u.user.isOrgMembershipActive ? (
|
||||
<span key={u.id}>{getMemberLabel(u)}</span>
|
||||
) : (
|
||||
<span className="opacity-40" key={u.id}>
|
||||
{getMemberLabel(u)}{" "}
|
||||
<span className="text-xs">
|
||||
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
|
||||
<div>
|
||||
(Inactive){" "}
|
||||
<FontAwesomeIcon size="xs" icon={faQuestionCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
|
||||
{el.groupLabels}
|
||||
|
||||
+103
-28
@@ -8,7 +8,8 @@ import {
|
||||
faCodeBranch,
|
||||
faComment,
|
||||
faFolder,
|
||||
faHourglass
|
||||
faHourglass,
|
||||
faUserSlash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -85,6 +86,7 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||
return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
|
||||
if (status === ApprovalStatus.REJECTED)
|
||||
return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
|
||||
|
||||
return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
|
||||
};
|
||||
|
||||
@@ -162,15 +164,21 @@ export const SecretApprovalRequestChanges = ({
|
||||
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
|
||||
|
||||
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||
Record<string, { status: ApprovalStatus; comment: string }>
|
||||
Record<string, { status: ApprovalStatus; comment: string; isOrgMembershipActive: boolean }>
|
||||
>(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr.userId]: { status: curr.status, comment: curr.comment }
|
||||
[curr.userId]: {
|
||||
status: curr.status,
|
||||
comment: curr.comment,
|
||||
isOrgMembershipActive: curr.isOrgMembershipActive
|
||||
}
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
console.log("reviewedUsers", reviewedUsers);
|
||||
|
||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus, comment: string) => {
|
||||
try {
|
||||
await updateSecretApprovalRequestStatus({
|
||||
@@ -524,7 +532,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-0 z-[51] w-1/5 cursor-default pt-2" style={{ minWidth: "240px" }}>
|
||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||
<div className="text-sm text-bunker-300">Reviewerssss</div>
|
||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||
{secretApprovalRequestDetails?.policy?.approvers
|
||||
.filter(
|
||||
@@ -533,26 +541,50 @@ export const SecretApprovalRequestChanges = ({
|
||||
)
|
||||
.map((requiredApprover) => {
|
||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||
const isOrgMembershipActive = requiredApprover.isOrgMembershipActive;
|
||||
|
||||
console.log("isOrgMembershipActive", isOrgMembershipActive);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-nowrap items-center justify-between space-x-2 rounded border border-mineshaft-600 bg-mineshaft-800 px-2 py-1"
|
||||
key={`required-approver-${requiredApprover.userId}`}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
requiredApprover.firstName
|
||||
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
|
||||
: undefined
|
||||
}
|
||||
position="left"
|
||||
sideOffset={10}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center gap-1 text-sm",
|
||||
!isOrgMembershipActive && "opacity-40"
|
||||
)}
|
||||
>
|
||||
<div className="flex text-sm">
|
||||
<div>{requiredApprover?.email}</div>
|
||||
<span className="text-red">*</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<Tooltip
|
||||
content={
|
||||
requiredApprover.firstName
|
||||
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
|
||||
: undefined
|
||||
}
|
||||
position="left"
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className="flex">
|
||||
<div>{requiredApprover?.email}</div>
|
||||
<span className="text-red">*</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{!isOrgMembershipActive && (
|
||||
<Tooltip
|
||||
className="relative !z-[500]"
|
||||
content="This user has been deactivated and no longer has an active organization membership."
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faUserSlash}
|
||||
size="xs"
|
||||
className="text-mineshaft-300"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{reviewer?.comment && (
|
||||
<Tooltip className="max-w-lg break-words" content={reviewer.comment}>
|
||||
<FontAwesomeIcon
|
||||
@@ -562,9 +594,21 @@ export const SecretApprovalRequestChanges = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip content={`Status: ${reviewer?.status || ApprovalStatus.PENDING}`}>
|
||||
{getReviewedStatusSymbol(reviewer?.status)}
|
||||
</Tooltip>
|
||||
<div className="flex gap-2">
|
||||
<Tooltip
|
||||
className="relative !z-[500]"
|
||||
content={
|
||||
<span className="text-sm">
|
||||
Status:{" "}
|
||||
<span className="capitalize">
|
||||
{reviewer?.status || ApprovalStatus.PENDING}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{getReviewedStatusSymbol(reviewer?.status)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -578,20 +622,43 @@ export const SecretApprovalRequestChanges = ({
|
||||
)
|
||||
.map((reviewer) => {
|
||||
const status = reviewedUsers?.[reviewer.userId].status;
|
||||
const isOrgMembershipActive = reviewer.isOrgMembershipActive;
|
||||
return (
|
||||
<div
|
||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||
className="flex flex-nowrap items-center justify-between space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||
key={`required-approver-${reviewer.userId}`}
|
||||
>
|
||||
<div className="flex-grow text-sm">
|
||||
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
|
||||
<span>{reviewer?.email} </span>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center gap-1 text-sm",
|
||||
!isOrgMembershipActive && "opacity-40"
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
className="relative !z-[500]"
|
||||
content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}
|
||||
>
|
||||
<div className="flex">
|
||||
<span>{reviewer?.email} </span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<span className="text-red">*</span>
|
||||
{!isOrgMembershipActive && (
|
||||
<Tooltip
|
||||
className="relative !z-[500]"
|
||||
content="This user has been deactivated and no longer has an active organization membership."
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faUserSlash}
|
||||
size="xs"
|
||||
className="text-mineshaft-300"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{reviewer.comment && (
|
||||
<Tooltip content={reviewer.comment}>
|
||||
<Tooltip className="relative !z-[500]" content={reviewer.comment}>
|
||||
<FontAwesomeIcon
|
||||
icon={faComment}
|
||||
size="xs"
|
||||
@@ -599,7 +666,15 @@ export const SecretApprovalRequestChanges = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip content={status || ApprovalStatus.PENDING}>
|
||||
<Tooltip
|
||||
className="relative !z-[500]"
|
||||
content={
|
||||
<span className="text-sm">
|
||||
Status:{" "}
|
||||
<span className="capitalize">{status || ApprovalStatus.PENDING}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{getReviewedStatusSymbol(status)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user