diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index c13351c13..076b33e54 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -211,6 +211,27 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "PATCH", + url: "/user-management/users/:userId/admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: 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.params.userId); + } + }); + server.route({ method: "GET", url: "/encryption-strategies", diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 9a6075423..166f73317 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -291,6 +291,15 @@ export const superAdminServiceFactory = ({ 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.updateById(userId, { superAdmin: true }); + }; + const getAdminSlackConfig = async () => { const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); @@ -381,6 +390,7 @@ export const superAdminServiceFactory = ({ deleteUser, getAdminSlackConfig, updateRootEncryptionStrategy, - getConfiguredEncryptionStrategies + getConfiguredEncryptionStrategies, + grantServerAdminAccessToUser }; }; diff --git a/docs/documentation/platform/admin-panel/server-admin.mdx b/docs/documentation/platform/admin-panel/server-admin.mdx index f0b173d46..137cdde59 100644 --- a/docs/documentation/platform/admin-panel/server-admin.mdx +++ b/docs/documentation/platform/admin-panel/server-admin.mdx @@ -76,8 +76,7 @@ This tab allows you to set various rate limits for your Infisical instance. You ## User Management Tab -From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by pressing the **X** button on their respective row. - +From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by clicking on the three dots icon on the right. Additionally, the Server Admin can grant server administrator access to other users through this menu. ![User Management](/images/platform/admin-panels/admin-panel-users.png) diff --git a/docs/images/platform/admin-panels/admin-panel-users.png b/docs/images/platform/admin-panels/admin-panel-users.png index 94add6d85..1f56f2f2d 100644 Binary files a/docs/images/platform/admin-panels/admin-panel-users.png and b/docs/images/platform/admin-panels/admin-panel-users.png differ diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 5405878c7..4d9f05bb1 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -3,7 +3,8 @@ export { useCreateAdminUser, useUpdateAdminSlackConfig, useUpdateServerConfig, - useUpdateServerEncryptionStrategy + useUpdateServerEncryptionStrategy, + useAdminGrantServerAdminAccess } from "./mutation"; export { useAdminGetUsers, diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index c68760fa4..901c079a0 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -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/${userId}/admin-access`); + return {}; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [adminStandaloneKeys.getUsers] + }); + } + }); +}; + export const useUpdateAdminSlackConfig = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 889420a73..df51216a6 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -1,5 +1,5 @@ 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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -9,7 +9,6 @@ import { Button, DeleteActionModal, EmptyState, - IconButton, Input, Table, TableContainer, @@ -18,21 +17,33 @@ import { Td, Th, THead, - Tr + Tr, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger } from "@app/components/v2"; import { useSubscription, useUser } from "@app/context"; 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"; +const deleteUserUpgradePlanMessage = "Deleting users via Admin UI"; +const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions"; + const UserPanelTable = ({ handlePopUpOpen }: { handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["removeUser", "upgradePlan"]>, + popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>, data?: { username: string; id: string; + message?: string; } ) => void; }) => { @@ -87,22 +98,49 @@ const UserPanelTable = ({ {userId !== id && (
- { - if (!subscription?.instanceUserManagement) { - handlePopUpOpen("upgradePlan"); - return; - } - handlePopUpOpen("removeUser", { username, id }); - }} - > - - + + +
+ +
+
+ + { + e.stopPropagation(); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: deleteUserUpgradePlanMessage + }); + return; + } + handlePopUpOpen("removeUser", { username, id }); + }} + > + Remove User + + {!superAdmin && ( + { + e.stopPropagation(); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: addServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("upgradeToServerAdmin", { username, id }); + }} + > + Make User Server Admin + + )} + +
)} @@ -134,10 +172,12 @@ const UserPanelTable = ({ export const UserPanel = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "removeUser", - "upgradePlan" + "upgradePlan", + "upgradeToServerAdmin" ] as const); const { mutateAsync: deleteUser } = useAdminDeleteUser(); + const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess(); const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; @@ -158,6 +198,25 @@ export const UserPanel = () => { 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 (
@@ -173,10 +232,21 @@ export const UserPanel = () => { onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)} onDeleteApproved={handleRemoveUser} /> + handlePopUpToggle("upgradeToServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleGrantServerAdminAccess} + buttonText="Grant Access" + /> handlePopUpToggle("upgradePlan", isOpen)} - text="Deleting users via Admin UI is only available on Infisical's Pro plan and above." + text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`} />
);