Allow server admins to grant server admin access to other users

This commit is contained in:
carlosmonastyrski
2025-03-04 12:38:27 -03:00
parent e5bca5b5df
commit be4adc2759
5 changed files with 134 additions and 24 deletions
@@ -211,6 +211,27 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "PATCH",
url: "/user-management/users/grant-admin-access",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
userId: z.string()
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
await server.services.superAdmin.grantServerAdminAccessToUser(req.body.userId);
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/encryption-strategies", url: "/encryption-strategies",
@@ -291,6 +291,18 @@ export const superAdminServiceFactory = ({
return user; return user;
}; };
const grantServerAdminAccessToUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({
message: "Failed to grant server admin access to user due to plan restriction. Upgrade to Infisical's Pro plan."
});
}
await userDAL.transaction(async (tx) => {
await userDAL.updateById(userId, { superAdmin: true }, tx);
});
};
const getAdminSlackConfig = async () => { const getAdminSlackConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
@@ -381,6 +393,7 @@ export const superAdminServiceFactory = ({
deleteUser, deleteUser,
getAdminSlackConfig, getAdminSlackConfig,
updateRootEncryptionStrategy, updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser
}; };
}; };
+2 -1
View File
@@ -3,7 +3,8 @@ export {
useCreateAdminUser, useCreateAdminUser,
useUpdateAdminSlackConfig, useUpdateAdminSlackConfig,
useUpdateServerConfig, useUpdateServerConfig,
useUpdateServerEncryptionStrategy useUpdateServerEncryptionStrategy,
useAdminGrantServerAdminAccess
} from "./mutation"; } from "./mutation";
export { export {
useAdminGetUsers, useAdminGetUsers,
+15
View File
@@ -70,6 +70,21 @@ export const useAdminDeleteUser = () => {
}); });
}; };
export const useAdminGrantServerAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userId: string) => {
await apiRequest.patch("/api/v1/admin/user-management/users/grant-admin-access", { userId });
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
}
});
};
export const useUpdateAdminSlackConfig = () => { export const useUpdateAdminSlackConfig = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({ return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { faMagnifyingGlass, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -9,7 +9,6 @@ import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
IconButton,
Input, Input,
Table, Table,
TableContainer, TableContainer,
@@ -18,18 +17,26 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr Tr,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2"; } from "@app/components/v2";
import { useSubscription, useUser } from "@app/context"; import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks"; import { useDebounce, usePopUp } from "@app/hooks";
import { useAdminDeleteUser, useAdminGetUsers } from "@app/hooks/api"; import {
useAdminDeleteUser,
useAdminGetUsers,
useAdminGrantServerAdminAccess
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const UserPanelTable = ({ const UserPanelTable = ({
handlePopUpOpen handlePopUpOpen
}: { }: {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeUser", "upgradePlan"]>, popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>,
data?: { data?: {
username: string; username: string;
id: string; id: string;
@@ -87,22 +94,43 @@ const UserPanelTable = ({
<Td> <Td>
{userId !== id && ( {userId !== id && (
<div className="flex justify-end"> <div className="flex justify-end">
<IconButton <DropdownMenu>
size="lg" <DropdownMenuTrigger asChild className="rounded-lg">
colorSchema="danger" <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
variant="plain" <FontAwesomeIcon size="sm" icon={faEllipsis} />
ariaLabel="update" </div>
isDisabled={userId === id} </DropdownMenuTrigger>
onClick={() => { <DropdownMenuContent align="start" className="p-1">
if (!subscription?.instanceUserManagement) { <DropdownMenuItem
handlePopUpOpen("upgradePlan"); className="hover:!bg-red-500 hover:!text-white"
return; onClick={(e) => {
} e.stopPropagation();
handlePopUpOpen("removeUser", { username, id }); if (!subscription?.instanceUserManagement) {
}} handlePopUpOpen("upgradePlan");
> return;
<FontAwesomeIcon icon={faXmark} /> }
</IconButton> handlePopUpOpen("removeUser", { username, id });
}}
>
Remove User
</DropdownMenuItem>
{(!superAdmin &&
<DropdownMenuItem
className="hover:!bg-yellow/20 hover:!text-yellow"
onClick={(e) => {
e.stopPropagation();
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("upgradeToServerAdmin", { username, id });
}}
>
Make User Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
)} )}
</Td> </Td>
@@ -134,10 +162,12 @@ const UserPanelTable = ({
export const UserPanel = () => { export const UserPanel = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser", "removeUser",
"upgradePlan" "upgradePlan",
"upgradeToServerAdmin"
] as const); ] as const);
const { mutateAsync: deleteUser } = useAdminDeleteUser(); const { mutateAsync: deleteUser } = useAdminDeleteUser();
const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess();
const handleRemoveUser = async () => { const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string }; const { id } = popUp?.removeUser?.data as { id: string; username: string };
@@ -158,6 +188,25 @@ export const UserPanel = () => {
handlePopUpClose("removeUser"); handlePopUpClose("removeUser");
}; };
const handleGrantServerAdminAccess = async () => {
const { id } = popUp?.upgradeToServerAdmin?.data as { id: string; username: string };
try {
await grantAdminAccess(id);
createNotification({
type: "success",
text: "Successfully granted server admin access to user"
});
} catch {
createNotification({
type: "error",
text: "Error granting server admin access to user"
});
}
handlePopUpClose("upgradeToServerAdmin");
};
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4"> <div className="mb-4">
@@ -173,6 +222,17 @@ export const UserPanel = () => {
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)} onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
onDeleteApproved={handleRemoveUser} onDeleteApproved={handleRemoveUser}
/> />
<DeleteActionModal
isOpen={popUp.upgradeToServerAdmin.isOpen}
title={`Are you sure want to grant Server Admin permissions to ${
(popUp?.upgradeToServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("upgradeToServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleGrantServerAdminAccess}
buttonText="Grant Access"
/>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}