From c119adb6dccedc1da78b30d91fa552e174e88191 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 23 Feb 2026 23:06:27 -0800 Subject: [PATCH] [Fix] UI - Virtual Keys: restrict Edit Settings button to key owners Non-owner Internal Users could see and interact with the "Edit Settings" button in the key Settings tab for keys they don't own. The button was gated by `rolesWithWriteAccess.includes(userRole)` (role-only check) instead of `canModifyKey` (ownership-aware), unlike the Regenerate and Delete buttons which already used the correct check. Replace the condition with `canModifyKey` so the Edit Settings button follows the same proxy-admin / team-admin / key-owner logic as the other action buttons. Add tests covering all permission paths. --- .../templates/key_info_view.test.tsx | 108 +++++++++++++++--- .../components/templates/key_info_view.tsx | 2 +- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index db43d732e0..bbb212e49b 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -363,31 +363,101 @@ describe("KeyInfoView", () => { }); - it("should show edit button in settings tab when user has write access", async () => { - vi.mocked(useAuthorized).mockReturnValue({ - ...baseUseAuthorizedMock, - userRole: "Admin", + describe("'Edit Settings' button visibility in the Settings tab", () => { + const renderAndOpenSettingsTab = async (keyData = MOCK_KEY_DATA) => { + render( + {}} + keyId="test-key-id" + onKeyDataUpdate={() => {}} + teams={[]} + />, + ); + await waitFor(() => { + expect(screen.getByRole("tab", { name: /settings/i })).toBeInTheDocument(); + }); + await userEvent.click(screen.getByRole("tab", { name: /settings/i })); + }; + + it("should show the Edit Settings button when the user is a proxy admin for a key they do not own", async () => { + vi.mocked(useAuthorized).mockReturnValue({ + ...baseUseAuthorizedMock, + userId: "proxy-admin-user-id", + userRole: "proxy_admin", + }); + + await renderAndOpenSettingsTab({ ...MOCK_KEY_DATA, user_id: "someone-else-id" }); + + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); }); - render( - { }} - keyId={"test-key-id"} - onKeyDataUpdate={() => { }} - teams={[]} - />, - ); + it("should show the Edit Settings button when the user is the key owner", async () => { + vi.mocked(useAuthorized).mockReturnValue({ + ...baseUseAuthorizedMock, + userId: "owner-user-id", + userRole: "Internal User", + }); - await waitFor(() => { - const settingsTab = screen.getByRole("tab", { name: /settings/i }); - expect(settingsTab).toBeInTheDocument(); + await renderAndOpenSettingsTab({ ...MOCK_KEY_DATA, user_id: "owner-user-id" }); + + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); }); - const settingsTab = screen.getByRole("tab", { name: /settings/i }); - await userEvent.click(settingsTab); + it("should not show the Edit Settings button when an Internal User does not own the key", async () => { + vi.mocked(useAuthorized).mockReturnValue({ + ...baseUseAuthorizedMock, + userId: "non-owner-user-id", + userRole: "Internal User", + }); + + await renderAndOpenSettingsTab({ ...MOCK_KEY_DATA, user_id: "owner-user-id" }); + + expect(screen.queryByRole("button", { name: /edit settings/i })).not.toBeInTheDocument(); + }); + + it("should not show the Edit Settings button when the user is an Internal Viewer even if they own the key", async () => { + vi.mocked(useAuthorized).mockReturnValue({ + ...baseUseAuthorizedMock, + userId: "owner-user-id", + userRole: "Internal Viewer", + }); + + await renderAndOpenSettingsTab({ ...MOCK_KEY_DATA, user_id: "owner-user-id" }); + + expect(screen.queryByRole("button", { name: /edit settings/i })).not.toBeInTheDocument(); + }); + + it("should show the Edit Settings button when the user is a team admin for the key's team", async () => { + const teamId = "test-team-id"; + const teamAdminUserId = "team-admin-user"; + vi.mocked(useTeams).mockReturnValue({ + teams: [ + { + team_id: teamId, + team_alias: "Test Team", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2025-01-01T00:00:00Z", + keys: [], + members_with_roles: [{ user_id: teamAdminUserId, role: "admin" }], + spend: 0, + }, + ], + setTeams: vi.fn(), + }); + vi.mocked(useAuthorized).mockReturnValue({ + ...baseUseAuthorizedMock, + userId: teamAdminUserId, + userRole: "user", + }); + + await renderAndOpenSettingsTab({ ...MOCK_KEY_DATA, team_id: teamId, user_id: "other-user-id" }); - await waitFor(() => { expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index f2a12c9087..94ca90b963 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -595,7 +595,7 @@ export default function KeyInfoView({
Key Settings - {!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && ( + {!isEditing && canModifyKey && ( )}