mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix: allow team admin to edit guardrails they create
This commit is contained in:
parent
94c8705d0c
commit
3061bd71c9
4 changed files with 101 additions and 5 deletions
|
|
@ -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";
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<GuardrailInfoView guardrailId="123" onClose={() => {}} 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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<GuardrailInfoProps> = ({ guardrailId, onClose, accessToken, isAdmin }) => {
|
||||
const { userId } = useAuthorized();
|
||||
const { data: teams } = useTeams();
|
||||
const [guardrailData, setGuardrailData] = useState<any>(null);
|
||||
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -463,6 +468,14 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ 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 (
|
||||
<div className="p-4">
|
||||
|
|
@ -490,7 +503,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
|
||||
{canEditGuardrail ? <Tab key="settings">Settings</Tab> : <></>}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -589,7 +602,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<CodeOutlined className="text-blue-500" />
|
||||
<Text className="font-medium text-lg">Custom Code</Text>
|
||||
</div>
|
||||
{isAdmin && !isConfigGuardrail && (
|
||||
{canEditGuardrail && !isConfigGuardrail && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CodeOutlined />}
|
||||
|
|
@ -616,8 +629,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* Settings Panel (only for admins) */}
|
||||
{isAdmin && (
|
||||
{/* Settings Panel (only for admins or team admins for this guardrail's team) */}
|
||||
{canEditGuardrail && (
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue