mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-14 12:26:25 +00:00
[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.
This commit is contained in:
@@ -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(
|
||||
<KeyInfoView
|
||||
keyData={keyData}
|
||||
onClose={() => {}}
|
||||
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(
|
||||
<KeyInfoView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onClose={() => { }}
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -595,7 +595,7 @@ export default function KeyInfoView({
|
||||
<Card className="overflow-y-auto max-h-[65vh]">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Key Settings</Title>
|
||||
{!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
{!isEditing && canModifyKey && (
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user