feat: implemented user role impersonation

This commit is contained in:
=
2025-04-21 18:43:21 +05:30
parent eba12912f8
commit 7219ba3b46
40 changed files with 750 additions and 51 deletions
@@ -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
);
@@ -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,29 @@ const Page = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteIdentity",
"upgradePlan"
"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 +135,23 @@ const Page = () => {
>
Copy Membership ID
</Button>
<ProjectPermissionCan
I={ProjectPermissionIdentityActions.AssumePrivileges}
a={ProjectPermissionSub.Identity}
renderTooltip
allowedLabel="Assume or impersonate privileges of the user"
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("assumePrivileges")}
>
Impersonate Identity
</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 impersonate 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" />
@@ -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 })
);
@@ -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
})
@@ -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,28 @@ 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 or impersonate privileges of the user"
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
isLoading={isRemovingUserFromWorkspace}
onClick={() =>
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
}
>
Impersonate User
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionMemberActions.Delete}
a={ProjectPermissionSub.Member}
renderTooltip
allowedLabel="Remove from project"
@@ -128,6 +184,15 @@ export const Page = () => {
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<ConfirmActionModal
isOpen={popUp.assumePrivileges.isOpen}
confirmKey="assume"
title="Do you want to impersonate 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)}
@@ -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>
@@ -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
);
@@ -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
);
@@ -577,6 +577,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 +588,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 +599,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 +612,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 +912,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 +923,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]: {
@@ -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
@@ -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