mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
Merge pull request #3447 from akhilmhdh/feat/assume-role
Implemented project permission impersonation
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { FormControl } from "../FormControl";
|
||||
import { Input } from "../Input";
|
||||
import { Modal, ModalClose, ModalContent } from "../Modal";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
confirmKey: string;
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
onConfirmed: () => Promise<void>;
|
||||
buttonText?: string;
|
||||
formContent?: ReactNode;
|
||||
children?: ReactNode;
|
||||
confirmationMessage?: ReactNode;
|
||||
};
|
||||
|
||||
export const ConfirmActionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onChange,
|
||||
confirmKey,
|
||||
onConfirmed,
|
||||
title,
|
||||
subTitle = "This action is irreversible.",
|
||||
buttonText = "Yes",
|
||||
formContent,
|
||||
confirmationMessage,
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
|
||||
useEffect(() => {
|
||||
setInputData("");
|
||||
}, [isOpen]);
|
||||
|
||||
const onDelete = async () => {
|
||||
setIsLoading.on();
|
||||
try {
|
||||
await onConfirmed();
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(isOpenState) => {
|
||||
setInputData("");
|
||||
if (onChange) onChange(isOpenState);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={title}
|
||||
subTitle={subTitle}
|
||||
footerContent={
|
||||
<div className="mx-2 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
isDisabled={!(confirmKey === inputData) || isLoading}
|
||||
onClick={onDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
{formContent}
|
||||
<form
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault();
|
||||
if (confirmKey === inputData) onDelete();
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
label={
|
||||
<div className="break-words pb-2 text-sm">
|
||||
{confirmationMessage || (
|
||||
<>
|
||||
Type <span className="font-bold">{confirmKey}</span> to perform this action
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input
|
||||
value={inputData}
|
||||
onChange={(e) => setInputData(e.target.value)}
|
||||
placeholder={`Type ${confirmKey} here`}
|
||||
/>
|
||||
</FormControl>
|
||||
{children}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ConfirmActionModal } from "./ConfirmActionModal";
|
||||
@@ -14,7 +14,7 @@ export const PageHeader = ({ title, description, children, className }: Props) =
|
||||
<div className="w-full">
|
||||
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
|
||||
</div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
<div className="mt-2 text-gray-400">{description}</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./Breadcrumb";
|
||||
export * from "./Button";
|
||||
export * from "./Card";
|
||||
export * from "./Checkbox";
|
||||
export * from "./ConfirmActionModal";
|
||||
export * from "./ContentLoader";
|
||||
export * from "./DatePicker";
|
||||
export * from "./DeleteActionModal";
|
||||
|
||||
@@ -14,12 +14,13 @@ export const useProjectPermission = () => {
|
||||
strict: false,
|
||||
select: (el) => el?.projectId
|
||||
});
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
||||
}
|
||||
|
||||
const {
|
||||
data: { permission, membership }
|
||||
data: { permission, membership, assumedPrivilegeDetails }
|
||||
} = useSuspenseQuery({
|
||||
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId: projectId }),
|
||||
queryFn: () => fetchUserProjectPermissions({ workspaceId: projectId }),
|
||||
@@ -29,6 +30,7 @@ export const useProjectPermission = () => {
|
||||
const ability = evaluatePermissionsAbility(rule);
|
||||
return {
|
||||
permission: ability,
|
||||
assumedPrivilegeDetails: data.assumedPrivilegeDetails,
|
||||
membership: {
|
||||
...data.membership,
|
||||
roles: data.membership.roles.map(({ role }) => role)
|
||||
@@ -42,5 +44,5 @@ export const useProjectPermission = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
return { permission, membership, hasProjectRole };
|
||||
return { permission, membership, hasProjectRole, assumedPrivilegeDetails };
|
||||
};
|
||||
|
||||
@@ -58,7 +58,8 @@ export enum ProjectPermissionIdentityActions {
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
GrantPrivileges = "grant-privileges",
|
||||
AssumePrivileges = "assume-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionMemberActions {
|
||||
@@ -66,7 +67,8 @@ export enum ProjectPermissionMemberActions {
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
GrantPrivileges = "grant-privileges",
|
||||
AssumePrivileges = "assume-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionGroupActions {
|
||||
@@ -247,7 +249,7 @@ export type ProjectPermissionSet =
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionMemberActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||
@@ -258,7 +260,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
(
|
||||
| ProjectPermissionSub.Identity
|
||||
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { useAssumeProjectPrivileges, useRemoveAssumeProjectPrivilege } from "./mutations";
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TProjectAssumePrivilegesDTO } from "./types";
|
||||
|
||||
export const useAssumeProjectPrivileges = () =>
|
||||
useMutation({
|
||||
mutationFn: async ({ projectId, actorId, actorType }: TProjectAssumePrivilegesDTO) => {
|
||||
const { data } = await apiRequest.post<{ message: string }>(
|
||||
`/api/v1/workspace/${projectId}/assume-privileges`,
|
||||
{ actorId, actorType }
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
export const useRemoveAssumeProjectPrivilege = () =>
|
||||
useMutation({
|
||||
mutationFn: async ({ projectId }: { projectId: string }) => {
|
||||
const { data } = await apiRequest.delete<{ message: string }>(
|
||||
`/api/v1/workspace/${projectId}/assume-privileges`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ActorType } from "../auditLogs/enums";
|
||||
|
||||
export type TProjectAssumePrivilegesDTO = {
|
||||
projectId: string;
|
||||
actorType: ActorType;
|
||||
actorId: string;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./accessApproval";
|
||||
export * from "./admin";
|
||||
export * from "./apiKeys";
|
||||
export * from "./assumePrivileges";
|
||||
export * from "./auditLogs";
|
||||
export * from "./auditLogStreams";
|
||||
export * from "./auth";
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/type
|
||||
import { groupBy } from "@app/lib/fn/array";
|
||||
import { omit } from "@app/lib/fn/object";
|
||||
|
||||
import { ActorType } from "../auditLogs/enums";
|
||||
import { OrgUser, TProjectMembership } from "../users/types";
|
||||
import {
|
||||
TGetUserOrgPermissionsDTO,
|
||||
@@ -137,6 +138,12 @@ export const fetchUserProjectPermissions = async ({
|
||||
data: {
|
||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
|
||||
assumedPrivilegeDetails?: {
|
||||
actorId: string;
|
||||
actorType: ActorType;
|
||||
actorEmail: string;
|
||||
actorName: string;
|
||||
};
|
||||
};
|
||||
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TBreadcrumbFormat
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
useProjectPermission,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
|
||||
import { ProjectSelect } from "./components/ProjectSelect";
|
||||
|
||||
// This is a generic layout shared by all types of projects.
|
||||
@@ -35,6 +37,7 @@ export const ProjectLayout = () => {
|
||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { subscription } = useSubscription();
|
||||
@@ -68,6 +71,7 @@ export const ProjectLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
|
||||
export const AssumePrivilegeModeBanner = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
|
||||
if (!assumedPrivilegeDetails) return null;
|
||||
|
||||
return (
|
||||
<div className="z-10 -mx-4 flex items-center justify-center gap-2 rounded border border-mineshaft-600 bg-primary-400 p-2 text-mineshaft-800 shadow">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mr-2" />
|
||||
You are currently viewing the project with privileges of{" "}
|
||||
<b>
|
||||
{assumedPrivilegeDetails?.actorType === ActorType.IDENTITY ? "identity" : "user"}{" "}
|
||||
{assumedPrivilegeDetails?.actorName}
|
||||
</b>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
className="hover:bg-mineshaft-500"
|
||||
onClick={() => {
|
||||
exitAssumePrivilegeMode.mutate(
|
||||
{
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
Click to exit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AssumePrivilegeModeBanner } from "./AssumePrivilegeModeBanner";
|
||||
+2
-1
@@ -27,6 +27,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
@@ -68,7 +69,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
||||
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
const { permission } = useProjectPermission();
|
||||
const isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
|
||||
+3
-1
@@ -37,6 +37,7 @@ import {
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
@@ -88,7 +89,8 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
] as const);
|
||||
const { permission } = useProjectPermission();
|
||||
const isMemberEditDisabled =
|
||||
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege;
|
||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member) &&
|
||||
Boolean(privilege);
|
||||
|
||||
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
||||
const requestAccess = useCreateAccessRequest();
|
||||
|
||||
@@ -6,13 +6,27 @@ import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
Button,
|
||||
ConfirmActionModal,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
PageHeader,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useAssumeProjectPrivileges,
|
||||
useDeleteIdentityFromWorkspace,
|
||||
useGetWorkspaceIdentityMembershipDetails
|
||||
} from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
|
||||
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection";
|
||||
@@ -35,8 +49,28 @@ const Page = () => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteIdentity",
|
||||
"upgradePlan"
|
||||
"assumePrivileges"
|
||||
] as const);
|
||||
const assumePrivileges = useAssumeProjectPrivileges();
|
||||
|
||||
const handleAssumePrivileges = async () => {
|
||||
assumePrivileges.mutate(
|
||||
{
|
||||
actorId: identityId,
|
||||
actorType: ActorType.IDENTITY,
|
||||
projectId: workspaceId
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Identity privilege assumption has started"
|
||||
});
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onRemoveIdentitySubmit = async () => {
|
||||
try {
|
||||
@@ -100,6 +134,24 @@ const Page = () => {
|
||||
>
|
||||
Copy Membership ID
|
||||
</Button>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Identity}
|
||||
renderTooltip
|
||||
allowedLabel="Assume privileges of the user"
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("assumePrivileges")}
|
||||
>
|
||||
Assume Privileges
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
@@ -137,6 +189,15 @@ const Page = () => {
|
||||
deleteKey="remove"
|
||||
onDeleteApproved={() => onRemoveIdentitySubmit()}
|
||||
/>
|
||||
<ConfirmActionModal
|
||||
isOpen={popUp.assumePrivileges.isOpen}
|
||||
confirmKey="assume"
|
||||
title="Do you want to assume privileges of this identity?"
|
||||
subTitle="This will set your privileges to those of the identity for the next hour."
|
||||
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
|
||||
onConfirmed={handleAssumePrivileges}
|
||||
buttonText="Confirm"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState title="Error: Unable to find the identity." className="py-12" />
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
@@ -97,7 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
});
|
||||
const { permission } = useProjectPermission();
|
||||
const isIdentityEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
|
||||
+7
-2
@@ -22,7 +22,12 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteIdentityProjectAdditionalPrivilege } from "@app/hooks/api";
|
||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||
@@ -83,7 +88,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
||||
identityId={identityId}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
@@ -66,7 +67,7 @@ export const IdentityRoleModify = ({ identityProjectMembership }: Props) => {
|
||||
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
const { permission } = useProjectPermission();
|
||||
const isIdentityEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
|
||||
@@ -6,15 +6,28 @@ import { formatRelative } from "date-fns";
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
ConfirmActionModal,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
PageHeader,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteUserFromWorkspace, useGetWorkspaceUserDetails } from "@app/hooks/api";
|
||||
import {
|
||||
useAssumeProjectPrivileges,
|
||||
useDeleteUserFromWorkspace,
|
||||
useGetWorkspaceUserDetails
|
||||
} from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
|
||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||
@@ -35,12 +48,34 @@ export const Page = () => {
|
||||
|
||||
const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } =
|
||||
useDeleteUserFromWorkspace();
|
||||
const assumePrivileges = useAssumeProjectPrivileges();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"removeMember",
|
||||
"upgradePlan"
|
||||
"upgradePlan",
|
||||
"assumePrivileges"
|
||||
] as const);
|
||||
|
||||
const handleAssumePrivileges = async () => {
|
||||
const { userId } = popUp?.assumePrivileges?.data as { userId: string };
|
||||
assumePrivileges.mutate(
|
||||
{
|
||||
actorId: userId,
|
||||
actorType: ActorType.USER,
|
||||
projectId: workspaceId
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "User privilege assumption has started"
|
||||
});
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveUser = async () => {
|
||||
if (!currentOrg?.id || !currentWorkspace?.id || !membershipDetails?.user?.username) return;
|
||||
|
||||
@@ -91,7 +126,29 @@ export const Page = () => {
|
||||
description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`}
|
||||
>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Assume privileges of the user"
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
isLoading={assumePrivileges.isPending}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
|
||||
}
|
||||
>
|
||||
Assume Privileges
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove from project"
|
||||
@@ -128,6 +185,15 @@ export const Page = () => {
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||
onDeleteApproved={handleRemoveUser}
|
||||
/>
|
||||
<ConfirmActionModal
|
||||
isOpen={popUp.assumePrivileges.isOpen}
|
||||
confirmKey="assume"
|
||||
title="Do you want to assume privileges of this user?"
|
||||
subTitle="This will set your privileges to those of the user for the next hour."
|
||||
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
|
||||
onConfirmed={handleAssumePrivileges}
|
||||
buttonText="Confirm"
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useUser
|
||||
@@ -90,7 +91,7 @@ export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: P
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={
|
||||
isOwnProjectMembershipDetails ||
|
||||
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member)
|
||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
@@ -95,7 +95,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
@@ -69,7 +70,7 @@ export const MemberRoleModify = ({ projectMember, onOpenUpgradeModal }: Props) =
|
||||
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
const { permission } = useProjectPermission();
|
||||
const isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
|
||||
+16
-4
@@ -92,7 +92,8 @@ const MemberPolicyActionSchema = z.object({
|
||||
[ProjectPermissionMemberActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionMemberActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionMemberActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionMemberActions.GrantPrivileges]: z.boolean().optional()
|
||||
[ProjectPermissionMemberActions.GrantPrivileges]: z.boolean().optional(),
|
||||
[ProjectPermissionMemberActions.AssumePrivileges]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const IdentityPolicyActionSchema = z.object({
|
||||
@@ -100,7 +101,8 @@ const IdentityPolicyActionSchema = z.object({
|
||||
[ProjectPermissionIdentityActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionIdentityActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionIdentityActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionIdentityActions.GrantPrivileges]: z.boolean().optional()
|
||||
[ProjectPermissionIdentityActions.GrantPrivileges]: z.boolean().optional(),
|
||||
[ProjectPermissionIdentityActions.AssumePrivileges]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const GroupPolicyActionSchema = z.object({
|
||||
@@ -577,6 +579,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const canEdit = action.includes(ProjectPermissionMemberActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionMemberActions.Delete);
|
||||
const canGrantPrivileges = action.includes(ProjectPermissionMemberActions.GrantPrivileges);
|
||||
const canAssumePrivileges = action.includes(ProjectPermissionMemberActions.AssumePrivileges);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
@@ -587,6 +590,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (canDelete) formVal[subject]![0][ProjectPermissionMemberActions.Delete] = true;
|
||||
if (canGrantPrivileges)
|
||||
formVal[subject]![0][ProjectPermissionMemberActions.GrantPrivileges] = true;
|
||||
if (canAssumePrivileges)
|
||||
formVal[subject]![0][ProjectPermissionMemberActions.AssumePrivileges] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -596,6 +601,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const canEdit = action.includes(ProjectPermissionIdentityActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionIdentityActions.Delete);
|
||||
const canGrantPrivileges = action.includes(ProjectPermissionIdentityActions.GrantPrivileges);
|
||||
const canAssumePrivileges = action.includes(
|
||||
ProjectPermissionIdentityActions.AssumePrivileges
|
||||
);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{ conditions: [] }];
|
||||
|
||||
@@ -606,6 +614,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (canDelete) formVal[subject]![0][ProjectPermissionIdentityActions.Delete] = true;
|
||||
if (canGrantPrivileges)
|
||||
formVal[subject]![0][ProjectPermissionIdentityActions.GrantPrivileges] = true;
|
||||
if (canAssumePrivileges)
|
||||
formVal[subject]![0][ProjectPermissionIdentityActions.AssumePrivileges] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -904,7 +914,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Add", value: ProjectPermissionMemberActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionMemberActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionMemberActions.Delete },
|
||||
{ label: "Grant Privileges", value: ProjectPermissionMemberActions.GrantPrivileges }
|
||||
{ label: "Grant Privileges", value: ProjectPermissionMemberActions.GrantPrivileges },
|
||||
{ label: "Assume Privileges", value: ProjectPermissionMemberActions.AssumePrivileges }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.Identity]: {
|
||||
@@ -914,7 +925,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Add", value: ProjectPermissionIdentityActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionIdentityActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionIdentityActions.Delete },
|
||||
{ label: "Grant Privileges", value: ProjectPermissionIdentityActions.GrantPrivileges }
|
||||
{ label: "Grant Privileges", value: ProjectPermissionIdentityActions.GrantPrivileges },
|
||||
{ label: "Assume Privileges", value: ProjectPermissionIdentityActions.AssumePrivileges }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.Groups]: {
|
||||
|
||||
+5
-2
@@ -25,7 +25,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
@@ -289,7 +289,10 @@ export const AccessApprovalRequest = ({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
|
||||
{!!permission.can(
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
|
||||
+5
-2
@@ -22,7 +22,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useUser,
|
||||
@@ -171,7 +171,10 @@ export const SecretApprovalRequest = () => {
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
|
||||
{!!permission.can(
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user