diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226200038_baseline_diff/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226200038_baseline_diff/migration.sql new file mode 100644 index 00000000000..36c92fc4134 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226200038_baseline_diff/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "LiteLLM_DeletedVerificationToken" DROP COLUMN "agent_id"; + +-- AlterTable +ALTER TABLE "LiteLLM_ObjectPermissionTable" DROP COLUMN "blocked_tools"; + +-- DropTable +DROP TABLE "LiteLLM_SpendLogToolIndex"; + diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index cf4bd5582c2..68013701cb7 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -936,6 +936,7 @@ async def get_guardrail_info( guardrail_name=result.get("guardrail_name"), litellm_params=masked_litellm_params, guardrail_info=dict(result.get("guardrail_info") or {}), + team_id=result.get("team_id"), created_at=result.get("created_at"), updated_at=result.get("updated_at"), guardrail_definition_location=guardrail_definition_location, diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_info.test.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_info.test.tsx index acf112cf9a9..9f33b96b117 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_info.test.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_info.test.tsx @@ -1,8 +1,18 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import * as networking from "@/components/networking"; import { fireEvent, render, waitFor } from "@testing-library/react"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import GuardrailInfoView from "./guardrail_info"; +// Mock useAuthorized and useTeams so we don't need QueryClientProvider / auth context +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: vi.fn(), +})); +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + useTeams: vi.fn(), +})); + // Mock the networking module vi.mock("@/components/networking", () => ({ getGuardrailInfo: vi.fn(), @@ -35,6 +45,11 @@ vi.mock("./content_filter/ContentFilterManager", () => ({ })); describe("Guardrail Info", () => { + beforeEach(() => { + vi.mocked(useAuthorized).mockReturnValue({ userId: "test-user" } as any); + vi.mocked(useTeams).mockReturnValue({ data: [] } as any); + }); + afterEach(() => { vi.clearAllMocks(); }); @@ -267,4 +282,62 @@ describe("Guardrail Info", () => { expect(secondCallArgs.litellm_params.patterns).toEqual(["new_pattern"]); expect(secondCallArgs.litellm_params.blocked_words).toEqual(["new_word"]); }); + + it("should show the Settings tab and Edit Settings button when user is team admin for the guardrail's team", async () => { + const teamAdminUserId = "team-admin-user"; + vi.mocked(useAuthorized).mockReturnValue({ userId: teamAdminUserId } as any); + vi.mocked(useTeams).mockReturnValue({ + data: [ + { + team_id: "team-1", + team_alias: "Team 1", + models: [], + max_budget: null, + budget_duration: null, + members_with_roles: [ + { user_id: teamAdminUserId, user_email: "admin@test.com", role: "admin" }, + ], + spend: 0, + }, + ], + } as any); + + vi.mocked(networking.getGuardrailInfo).mockResolvedValue({ + guardrail_id: "123", + guardrail_name: "Team Guardrail", + team_id: "team-1", + litellm_params: { + guardrail: "presidio", + mode: "pre_call", + default_on: true, + }, + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + guardrail_definition_location: "database", + }); + + vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({ + supported_entities: ["PERSON", "EMAIL"], + supported_actions: ["MASK", "REDACT"], + pii_entity_categories: [], + supported_modes: ["pre_call", "post_call"], + }); + + vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({}); + + const { getByText } = render( + {}} accessToken="123" isAdmin={false} />, + ); + + await waitFor(() => { + expect(getByText("Settings")).toBeInTheDocument(); + }); + fireEvent.click(getByText("Settings")); + + await waitFor(() => { + expect(getByText("Guardrail Settings")).toBeInTheDocument(); + }); + + expect(getByText("Edit Settings")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx index 2151a91d9d7..4a2f796603b 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx @@ -1,3 +1,5 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { getGuardrailInfo, getGuardrailProviderSpecificParams, @@ -5,6 +7,7 @@ import { updateGuardrailCall, } from "@/components/networking"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; +import { isUserTeamAdminForSingleTeam } from "@/utils/roles"; import { CodeOutlined, EyeInvisibleOutlined, InfoCircleOutlined, StopOutlined } from "@ant-design/icons"; import { ArrowLeftIcon } from "@heroicons/react/outline"; import { @@ -57,6 +60,8 @@ interface ProviderParamsResponse { } const GuardrailInfoView: React.FC = ({ guardrailId, onClose, accessToken, isAdmin }) => { + const { userId } = useAuthorized(); + const { data: teams } = useTeams(); const [guardrailData, setGuardrailData] = useState(null); const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState(null); const [loading, setLoading] = useState(true); @@ -463,6 +468,14 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, }; const isConfigGuardrail = guardrailData.guardrail_definition_location === "config"; + const canEditGuardrail = + isAdmin || + (guardrailData?.team_id && + teams && + isUserTeamAdminForSingleTeam( + teams.find((t) => t.team_id === guardrailData.team_id)?.members_with_roles ?? null, + userId ?? "", + )); return (
@@ -490,7 +503,7 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, Overview - {isAdmin ? Settings : <>} + {canEditGuardrail ? Settings : <>} @@ -589,7 +602,7 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, Custom Code
- {isAdmin && !isConfigGuardrail && ( + {canEditGuardrail && !isConfigGuardrail && (