mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #36335 from BerriAI/litellm_/sleepy-nightingale-0d66fe
fix(ui): gate policy and prompt lookups on an admin capability
This commit is contained in:
commit
8335dbbcfd
16 changed files with 530 additions and 219 deletions
|
|
@ -25,6 +25,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import GuardrailSelector from "@/components/guardrails/GuardrailSelector";
|
||||
import PolicySelector from "@/components/policies/PolicySelector";
|
||||
import MCPToolArgumentsForm, { MCPToolArgumentsFormRef } from "@/components/mcp_tools/MCPToolArgumentsForm";
|
||||
|
|
@ -106,6 +107,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
simplified = false,
|
||||
fixedModel,
|
||||
}) => {
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const [mcpServers, setMCPServers] = useState<MCPServer[]>([]);
|
||||
const [mcpToolsets, setMCPToolsets] = useState<MCPToolset[]>([]);
|
||||
const [isToolsetsInfoModalVisible, setIsToolsetsInfoModalVisible] = useState(false);
|
||||
|
|
@ -1652,32 +1654,34 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<SafetyOutlined className="mr-2" /> Policies
|
||||
<Tooltip
|
||||
className="ml-1"
|
||||
title={
|
||||
<span>
|
||||
Select policy/policies to apply to this LLM API call. Policies define which guardrails are
|
||||
applied based on conditions. You can set up your policies{" "}
|
||||
<a href="?page=policies" style={{ color: "#1890ff" }}>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<PolicySelector
|
||||
value={selectedPolicies}
|
||||
onChange={setSelectedPolicies}
|
||||
className="mb-4"
|
||||
accessToken={accessToken || ""}
|
||||
/>
|
||||
</div>
|
||||
{canViewPolicies && (
|
||||
<div>
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<SafetyOutlined className="mr-2" /> Policies
|
||||
<Tooltip
|
||||
className="ml-1"
|
||||
title={
|
||||
<span>
|
||||
Select policy/policies to apply to this LLM API call. Policies define which guardrails are
|
||||
applied based on conditions. You can set up your policies{" "}
|
||||
<a href="?page=policies" style={{ color: "#1890ff" }}>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<PolicySelector
|
||||
value={selectedPolicies}
|
||||
onChange={setSelectedPolicies}
|
||||
className="mb-4"
|
||||
accessToken={accessToken || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Code Interpreter Toggle - Only for Responses endpoint */}
|
||||
{endpointType === EndpointType.RESPONSES && (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
type ComplianceFramework,
|
||||
type CompliancePrompt,
|
||||
} from "@/data/compliancePrompts";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import { getGuardrailsList, testPoliciesAndGuardrails } from "@/components/networking";
|
||||
import PolicySelector, { getPolicyOptionEntries } from "@/components/policies/PolicySelector";
|
||||
import { Policy } from "@/components/policies/types";
|
||||
|
|
@ -123,6 +124,7 @@ export default function ComplianceUI({
|
|||
fixedModel,
|
||||
proxySettings,
|
||||
}: ComplianceUIProps) {
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const frameworks = getFrameworks();
|
||||
|
||||
const [policyValueToLabel, setPolicyValueToLabel] = useState<Map<string, string>>(new Map());
|
||||
|
|
@ -701,29 +703,37 @@ export default function ComplianceUI({
|
|||
<div className="shrink-0 border-b border-gray-200 px-6 py-4">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Test Configuration</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Select policies, guardrails, or both to test against.</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{canViewPolicies
|
||||
? "Select policies, guardrails, or both to test against."
|
||||
: "Select guardrails to test against."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="text-[11px] font-medium text-gray-500 uppercase tracking-wide mb-1.5 block">
|
||||
Policies
|
||||
</label>
|
||||
{accessToken && (
|
||||
<PolicySelector
|
||||
value={selectedPolicies}
|
||||
onChange={setSelectedPolicies}
|
||||
accessToken={accessToken}
|
||||
onPoliciesLoaded={handlePoliciesLoaded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{canViewPolicies && (
|
||||
<>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="text-[11px] font-medium text-gray-500 uppercase tracking-wide mb-1.5 block">
|
||||
Policies
|
||||
</label>
|
||||
{accessToken && (
|
||||
<PolicySelector
|
||||
value={selectedPolicies}
|
||||
onChange={setSelectedPolicies}
|
||||
accessToken={accessToken}
|
||||
onPoliciesLoaded={handlePoliciesLoaded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center pt-6 shrink-0">
|
||||
<div className="w-px h-4 bg-gray-200" />
|
||||
<span className="text-[10px] font-medium text-gray-400 my-1">or</span>
|
||||
<div className="w-px h-4 bg-gray-200" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center pt-6 shrink-0">
|
||||
<div className="w-px h-4 bg-gray-200" />
|
||||
<span className="text-[10px] font-medium text-gray-400 my-1">or</span>
|
||||
<div className="w-px h-4 bg-gray-200" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="text-[11px] font-medium text-gray-500 uppercase tracking-wide mb-1.5 block">
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import { useTeamMetadataSchema } from "@/app/(dashboard)/hooks/teams/useTeamMetadataSchema";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { fetchMCPAccessGroups, getGuardrailsList, teamCreateCall } from "./networking";
|
||||
import { fetchMCPAccessGroups, getGuardrailsList, getPoliciesList, teamCreateCall } from "./networking";
|
||||
import Teams from "./Teams";
|
||||
|
||||
const can = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({
|
||||
default: (...args: unknown[]) => can(...args),
|
||||
}));
|
||||
|
||||
const mockTeamInfoView = vi.fn();
|
||||
const mockUseOrganizations = vi.fn();
|
||||
|
||||
|
|
@ -173,6 +178,7 @@ const renderWithQueryClient = (
|
|||
// Re-establish safe defaults before every test (clearAllMocks keeps return values, so restore them here).
|
||||
beforeEach(() => {
|
||||
mockTeamsTableProps = null;
|
||||
can.mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("Teams - handleCreate organization handling", () => {
|
||||
|
|
@ -956,3 +962,50 @@ describe("Teams - LIT-2530 organization stays optional for proxy admin with a si
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Teams - policies field is gated on the viewPolicies capability", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockTeamInfoView.mockClear();
|
||||
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4"]);
|
||||
vi.mocked(fetchMCPAccessGroups).mockResolvedValue([]);
|
||||
vi.mocked(getGuardrailsList).mockResolvedValue({ guardrails: [] });
|
||||
vi.mocked(getPoliciesList).mockResolvedValue({ policies: [] });
|
||||
mockUseOrganizations.mockReturnValue({ data: null });
|
||||
});
|
||||
|
||||
const openAdditionalSettings = async () => {
|
||||
renderWithQueryClient(<Teams accessToken="test-token" userID="user-123" userRole="Admin" />);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getAllByRole("button", { name: /create team/i })[0]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/team name/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Additional Settings"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("access-group-selector")).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
it("should render the policies field and load it when the capability is present", async () => {
|
||||
await openAdditionalSettings();
|
||||
|
||||
expect(can).toHaveBeenCalledWith("viewPolicies");
|
||||
expect(getPoliciesList).toHaveBeenCalledWith("test-token");
|
||||
expect(screen.getByText("Policies")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should omit the policies field and skip the admin-only list without the capability", async () => {
|
||||
can.mockReturnValue(false);
|
||||
|
||||
await openAdditionalSettings();
|
||||
|
||||
expect(getPoliciesList).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("Policies")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import AvailableTeamsPanel from "@/components/team/AvailableTeamsPanel";
|
||||
import TeamInfoView from "@/components/team/TeamInfo";
|
||||
import TeamSSOSettings from "@/components/TeamSSOSettings";
|
||||
|
|
@ -108,6 +109,7 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
const [isTeamDeleting, setIsTeamDeleting] = useState(false);
|
||||
// Add this state near the other useState declarations
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const [policiesList, setPoliciesList] = useState<string[]>([]);
|
||||
const [loggingSettings, setLoggingSettings] = useState<any[]>([]);
|
||||
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
|
||||
|
|
@ -168,8 +170,8 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
};
|
||||
|
||||
fetchGuardrails();
|
||||
fetchPolicies();
|
||||
}, [accessToken]);
|
||||
if (canViewPolicies) fetchPolicies();
|
||||
}, [accessToken, canViewPolicies]);
|
||||
|
||||
const handleOk = () => {
|
||||
setIsTeamModalVisible(false);
|
||||
|
|
@ -795,36 +797,38 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this team to control guardrails and other settings">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
className="mt-8"
|
||||
help="Select existing policies or enter new ones"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select or enter policies"
|
||||
options={policiesList.map((name) => ({
|
||||
value: name,
|
||||
label: name,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
{canViewPolicies && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this team to control guardrails and other settings">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
className="mt-8"
|
||||
help="Select existing policies or enter new ones"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select or enter policies"
|
||||
options={policiesList.map((name) => ({
|
||||
value: name,
|
||||
label: name,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ describe("Sidebar (leftnav)", () => {
|
|||
userId: "internal-user-id",
|
||||
accessToken: "test-access-token",
|
||||
userRole: "internal",
|
||||
isViewOnly: false,
|
||||
token: "test-token",
|
||||
userEmail: "internal@example.com",
|
||||
premiumUser: false,
|
||||
|
|
@ -244,6 +245,27 @@ describe("Sidebar (leftnav)", () => {
|
|||
expect(screen.getByText("Tool Policies")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should hide the Policies entry from internal users while keeping Guardrails", () => {
|
||||
mockUseAuthorized.mockReturnValue(internalAuth);
|
||||
renderWithProviders(<Sidebar {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Guardrails")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Policies")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide the Prompts entry from internal users while keeping other Experimental children", async () => {
|
||||
mockUseAuthorized.mockReturnValue(internalAuth);
|
||||
renderWithProviders(<Sidebar {...defaultProps} />);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText("Experimental"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("API Playground")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText("Prompts")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show Organizations tab for organization admins", () => {
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ const menuGroups: MenuGroup[] = [
|
|||
page: "policies",
|
||||
label: "Policies",
|
||||
icon: <ScrollText {...ICON} />,
|
||||
roles: all_admin_roles,
|
||||
roles: rolesWithCapability("viewPolicies"),
|
||||
},
|
||||
{
|
||||
key: "tools",
|
||||
|
|
@ -268,7 +268,13 @@ const menuGroups: MenuGroup[] = [
|
|||
label: "Experimental",
|
||||
icon: <FlaskConical {...ICON} />,
|
||||
children: [
|
||||
{ key: "prompts", page: "prompts", label: "Prompts", icon: <FileText {...ICON} />, roles: all_admin_roles },
|
||||
{
|
||||
key: "prompts",
|
||||
page: "prompts",
|
||||
label: "Prompts",
|
||||
icon: <FileText {...ICON} />,
|
||||
roles: rolesWithCapability("viewPrompts"),
|
||||
},
|
||||
{
|
||||
key: "transform-request",
|
||||
page: "transform-request",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { act, fireEvent, within } from "@testing-library/react";
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
|
||||
import { Team } from "../key_team_helpers/key_list";
|
||||
import { userFilterUICall } from "../networking";
|
||||
import { getPoliciesList, getPromptsList, userFilterUICall } from "../networking";
|
||||
import CreateKey from "./create_key_button";
|
||||
|
||||
const { formMock, setFieldsValueMock, radioGroupValueRef, formStateRef, mockKeyCreateCall, teamDropdownTeamsRef } =
|
||||
|
|
@ -777,4 +777,47 @@ describe("CreateKey", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy and prompt fields", () => {
|
||||
const POLICIES_PLACEHOLDER = "Premium feature - Upgrade to set policies by key";
|
||||
const PROMPTS_PLACEHOLDER = "Premium feature - Upgrade to set prompts by key";
|
||||
|
||||
const openModal = () => {
|
||||
renderWithProviders(<CreateKey {...defaultProps} />);
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /create new key/i }));
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getPoliciesList).mockResolvedValue({ policies: [{ policy_name: "policy-a" }] });
|
||||
vi.mocked(getPromptsList).mockResolvedValue({ prompts: [{ prompt_id: "prompt-a" }] } as any);
|
||||
});
|
||||
|
||||
it("should load and offer both selectors for an admin", async () => {
|
||||
openModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("option", { name: "policy-a" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "prompt-a" })).toBeInTheDocument();
|
||||
});
|
||||
expect(getPoliciesList).toHaveBeenCalledWith("test-token");
|
||||
expect(getPromptsList).toHaveBeenCalledWith("test-token");
|
||||
expect(screen.getByPlaceholderText(POLICIES_PLACEHOLDER)).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(PROMPTS_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should omit both selectors and fire neither admin-only request for an internal user", async () => {
|
||||
authorizedState = { ...defaultAuthorizedState, userRole: "Internal User" };
|
||||
|
||||
openModal();
|
||||
|
||||
expect(await screen.findByTestId("org-dropdown")).toBeInTheDocument();
|
||||
|
||||
expect(getPoliciesList).not.toHaveBeenCalled();
|
||||
expect(getPromptsList).not.toHaveBeenCalled();
|
||||
expect(screen.queryByPlaceholderText(POLICIES_PLACEHOLDER)).not.toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText(PROMPTS_PLACEHOLDER)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
|
|||
import { useTags } from "@/app/(dashboard)/hooks/tags/useTags";
|
||||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -147,6 +148,8 @@ export const fetchUserModels = async (
|
|||
const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOpenCreate, prefillData }) => {
|
||||
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
|
||||
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const canViewPrompts = useCan("viewPrompts");
|
||||
const { data: organizations, isLoading: isOrganizationsLoading } = useOrganizations();
|
||||
const { data: projects, isLoading: isProjectsLoading } = useProjects();
|
||||
const { data: uiSettingsData } = useUISettings();
|
||||
|
|
@ -275,9 +278,9 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
};
|
||||
|
||||
fetchGuardrails();
|
||||
fetchPolicies();
|
||||
fetchPrompts();
|
||||
}, [accessToken]);
|
||||
if (canViewPolicies) fetchPolicies();
|
||||
if (canViewPrompts) fetchPrompts();
|
||||
}, [accessToken, canViewPolicies, canViewPrompts]);
|
||||
|
||||
// Fetch possible user roles when component mounts
|
||||
useEffect(() => {
|
||||
|
|
@ -1251,74 +1254,78 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
>
|
||||
<Switch disabled={!canEditGuardrails} checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this key to control guardrails and other settings">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
className="mt-4"
|
||||
help={
|
||||
premiumUser
|
||||
? "Select existing policies or enter new ones"
|
||||
: "Premium feature - Upgrade to set policies by key"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser ? "Premium feature - Upgrade to set policies by key" : "Select or enter policies"
|
||||
{canViewPolicies && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this key to control guardrails and other settings">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
options={policiesList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Prompts{" "}
|
||||
<Tooltip title="Allow this key to use specific prompt templates">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/prompt_management"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="prompts"
|
||||
className="mt-4"
|
||||
help={
|
||||
premiumUser
|
||||
? "Select existing prompts or enter new ones"
|
||||
: "Premium feature - Upgrade to set prompts by key"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser ? "Premium feature - Upgrade to set prompts by key" : "Select or enter prompts"
|
||||
name="policies"
|
||||
className="mt-4"
|
||||
help={
|
||||
premiumUser
|
||||
? "Select existing policies or enter new ones"
|
||||
: "Premium feature - Upgrade to set policies by key"
|
||||
}
|
||||
options={promptsList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser ? "Premium feature - Upgrade to set policies by key" : "Select or enter policies"
|
||||
}
|
||||
options={policiesList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{canViewPrompts && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Prompts{" "}
|
||||
<Tooltip title="Allow this key to use specific prompt templates">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/prompt_management"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="prompts"
|
||||
className="mt-4"
|
||||
help={
|
||||
premiumUser
|
||||
? "Select existing prompts or enter new ones"
|
||||
: "Premium feature - Upgrade to set prompts by key"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser ? "Premium feature - Upgrade to set prompts by key" : "Select or enter prompts"
|
||||
}
|
||||
options={promptsList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import { Policy } from "./types";
|
|||
|
||||
vi.mock("../networking");
|
||||
|
||||
const can = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({
|
||||
default: (...args: unknown[]) => can(...args),
|
||||
}));
|
||||
|
||||
const makePolicy = (overrides: Partial<Policy>): Policy => ({
|
||||
policy_id: "uuid-1",
|
||||
policy_name: "test-policy",
|
||||
|
|
@ -76,6 +81,7 @@ describe("PolicySelector", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
can.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
|
|
@ -114,4 +120,18 @@ describe("PolicySelector", () => {
|
|||
renderWithProviders(<PolicySelector accessToken="" onChange={mockOnChange} />);
|
||||
expect(networking.getPoliciesList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render nothing and skip the admin-only fetch without the viewPolicies capability", async () => {
|
||||
can.mockReturnValue(false);
|
||||
vi.mocked(networking.getPoliciesList).mockResolvedValue({ policies: [] });
|
||||
|
||||
const { container } = renderWithProviders(<PolicySelector accessToken="tok" onChange={mockOnChange} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(can).toHaveBeenCalledWith("viewPolicies");
|
||||
});
|
||||
expect(networking.getPoliciesList).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import { Policy } from "./types";
|
||||
import { getPoliciesList } from "../networking";
|
||||
|
||||
|
|
@ -51,12 +52,13 @@ const PolicySelector: React.FC<PolicySelectorProps> = ({
|
|||
disabled,
|
||||
onPoliciesLoaded,
|
||||
}) => {
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const [policies, setPolicies] = useState<Policy[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPolicies = async () => {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken || !canViewPolicies) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
|
|
@ -73,12 +75,16 @@ const PolicySelector: React.FC<PolicySelectorProps> = ({
|
|||
};
|
||||
|
||||
fetchPolicies();
|
||||
}, [accessToken, onPoliciesLoaded]);
|
||||
}, [accessToken, canViewPolicies, onPoliciesLoaded]);
|
||||
|
||||
const handlePolicyChange = (selectedValues: string[]) => {
|
||||
onChange(selectedValues);
|
||||
};
|
||||
|
||||
if (!canViewPolicies) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ vi.mock("@/components/networking", () => ({
|
|||
getPassThroughEndpointsCall: vi.fn(),
|
||||
}));
|
||||
|
||||
const can = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({
|
||||
default: (...args: unknown[]) => can(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/utils/dataUtils", () => ({
|
||||
copyToClipboard: vi.fn().mockResolvedValue(true),
|
||||
formatNumberWithCommas: vi.fn((value: number) => value.toLocaleString()),
|
||||
|
|
@ -242,6 +247,7 @@ describe("TeamInfoView", () => {
|
|||
} as any);
|
||||
vi.mocked(useTeamMetadataSchema).mockReturnValue({ data: [], isLoading: false } as any);
|
||||
|
||||
can.mockReturnValue(true);
|
||||
vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] });
|
||||
vi.mocked(networking.getPoliciesList).mockResolvedValue({ policies: [] });
|
||||
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
|
||||
|
|
@ -647,6 +653,43 @@ describe("TeamInfoView", () => {
|
|||
});
|
||||
|
||||
describe("settings and editing", () => {
|
||||
const policiesFormFieldLabel = () => screen.queryByText("Policies", { selector: "span" });
|
||||
|
||||
it("should offer the policies field and load it for a caller with the viewPolicies capability", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData());
|
||||
|
||||
renderWithProviders(<TeamInfoView {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networking.getPoliciesList).toHaveBeenCalled();
|
||||
});
|
||||
expect(can).toHaveBeenCalledWith("viewPolicies");
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Settings" }));
|
||||
await user.click(await screen.findByRole("button", { name: /edit settings/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(policiesFormFieldLabel()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit the policies field and skip the admin-only list without the capability", async () => {
|
||||
can.mockReturnValue(false);
|
||||
const user = userEvent.setup({ delay: null });
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData());
|
||||
|
||||
renderWithProviders(<TeamInfoView {...defaultProps} />);
|
||||
|
||||
await user.click(await screen.findByRole("tab", { name: "Settings" }));
|
||||
await user.click(await screen.findByRole("button", { name: /edit settings/i }));
|
||||
|
||||
expect(await screen.findByLabelText("Team Name")).toBeInTheDocument();
|
||||
|
||||
expect(networking.getPoliciesList).not.toHaveBeenCalled();
|
||||
expect(policiesFormFieldLabel()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open edit mode when edit button is clicked", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import { organizationKeys, useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import UserSearchModal from "@/components/common_components/user_search_modal";
|
||||
|
|
@ -199,6 +200,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
const { data: guardrailsData, isLoading: isGuardrailsLoading } = useGuardrails();
|
||||
const globalGuardrailNames = guardrailsData?.globalGuardrailNames ?? new Set<string>();
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const [policiesList, setPoliciesList] = useState<string[]>([]);
|
||||
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
|
||||
const [loadingPolicies, setLoadingPolicies] = useState(false);
|
||||
|
|
@ -298,8 +300,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
fetchPolicies();
|
||||
}, [accessToken]);
|
||||
if (canViewPolicies) fetchPolicies();
|
||||
}, [accessToken, canViewPolicies]);
|
||||
|
||||
// Fetch resolved guardrails for all policies
|
||||
useEffect(() => {
|
||||
|
|
@ -1338,30 +1340,32 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<Switch checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this team to control guardrails and other settings">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Select or enter policies"
|
||||
options={policiesList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
{canViewPolicies && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this team to control guardrails and other settings">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Select or enter policies"
|
||||
options={policiesList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@ import userEvent from "@testing-library/user-event";
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import { modelAvailableCall } from "../networking";
|
||||
import { getPoliciesList, getPromptsList, modelAvailableCall } from "../networking";
|
||||
import { KeyEditView } from "./key_edit_view";
|
||||
|
||||
const can = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({
|
||||
default: (...args: unknown[]) => can(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../networking", async () => {
|
||||
const actual = await vi.importActual("../networking");
|
||||
return {
|
||||
|
|
@ -212,6 +217,45 @@ describe("KeyEditView", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
can.mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("policy and prompt fields", () => {
|
||||
const renderAs = (userRole: string) =>
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onCancel={() => {}}
|
||||
onSubmit={async () => {}}
|
||||
accessToken="test-token"
|
||||
userID="user-123"
|
||||
userRole={userRole}
|
||||
premiumUser={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
it("should render both fields and load prompts for an admin", async () => {
|
||||
renderAs("Admin");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getPromptsList).toHaveBeenCalledWith("test-token");
|
||||
});
|
||||
expect(screen.getByText("Prompts", { selector: "label" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Policies")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should omit both fields and fire neither admin-only request for an internal user", async () => {
|
||||
renderAs("Internal User");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(modelAvailableCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getPromptsList).not.toHaveBeenCalled();
|
||||
expect(getPoliciesList).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("Prompts", { selector: "label" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Policies")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call onCancel when cancel button is clicked", async () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
|
|||
import { TextInput, Button as TremorButton } from "@tremor/react";
|
||||
import { Form, Input, Select, Switch, Tooltip } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { hasCapability } from "../../utils/capabilities";
|
||||
import { isProxyAdminRole, rolesWithWriteAccess } from "../../utils/roles";
|
||||
import AgentSelector from "../agent_management/AgentSelector";
|
||||
import AccessGroupSelector from "../common_components/AccessGroupSelector";
|
||||
|
|
@ -62,6 +63,8 @@ export function KeyEditView({
|
|||
premiumUser = false,
|
||||
}: KeyEditViewProps) {
|
||||
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
|
||||
const canViewPolicies = hasCapability(userRole, "viewPolicies");
|
||||
const canViewPrompts = hasCapability(userRole, "viewPrompts");
|
||||
const canEditEstimates = userRole != null && isProxyAdminRole(userRole);
|
||||
const estimateTooltip = estimateTooltips(canEditEstimates);
|
||||
const [form] = Form.useForm();
|
||||
|
|
@ -129,9 +132,9 @@ export function KeyEditView({
|
|||
}
|
||||
};
|
||||
|
||||
fetchPrompts();
|
||||
if (canViewPrompts) fetchPrompts();
|
||||
fetchModels();
|
||||
}, [userID, userRole, accessToken, team, keyData.team_id]);
|
||||
}, [userID, userRole, accessToken, team, keyData.team_id, canViewPrompts]);
|
||||
|
||||
// Sync disabled callbacks with form when component mounts
|
||||
useEffect(() => {
|
||||
|
|
@ -599,27 +602,29 @@ export function KeyEditView({
|
|||
<Switch disabled={!canEditGuardrails} checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this key to control guardrails and other settings">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
>
|
||||
{accessToken && (
|
||||
<PolicySelector
|
||||
onChange={(v) => {
|
||||
form.setFieldValue("policies", v);
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
disabled={!premiumUser}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
{canViewPolicies && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Policies{" "}
|
||||
<Tooltip title="Apply policies to this key to control guardrails and other settings">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
>
|
||||
{accessToken && (
|
||||
<PolicySelector
|
||||
onChange={(v) => {
|
||||
form.setFieldValue("policies", v);
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
disabled={!premiumUser}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item label="Tags" name="tags">
|
||||
<Select
|
||||
|
|
@ -634,23 +639,25 @@ export function KeyEditView({
|
|||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Prompts" name="prompts">
|
||||
<Tooltip title={!premiumUser ? "Setting prompts by key is a premium feature" : ""} placement="top">
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser
|
||||
? "Premium feature - Upgrade to set prompts by key"
|
||||
: Array.isArray(keyData.metadata?.prompts) && keyData.metadata.prompts.length > 0
|
||||
? `Current: ${keyData.metadata.prompts.join(", ")}`
|
||||
: "Select or enter prompts"
|
||||
}
|
||||
options={promptsList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
{canViewPrompts && (
|
||||
<Form.Item label="Prompts" name="prompts">
|
||||
<Tooltip title={!premiumUser ? "Setting prompts by key is a premium feature" : ""} placement="top">
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser
|
||||
? "Premium feature - Upgrade to set prompts by key"
|
||||
: Array.isArray(keyData.metadata?.prompts) && keyData.metadata.prompts.length > 0
|
||||
? `Current: ${keyData.metadata.prompts.join(", ")}`
|
||||
: "Select or enter prompts"
|
||||
}
|
||||
options={promptsList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
|
|||
|
|
@ -16,6 +16,42 @@ describe("hasCapability", () => {
|
|||
expect(hasCapability(role, "viewToolPolicies")).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer"])("should grant viewPolicies to %s", (role) => {
|
||||
expect(hasCapability(role, "viewPolicies")).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"Internal User",
|
||||
"Internal Viewer",
|
||||
"internal_user",
|
||||
"App User",
|
||||
"Org Admin",
|
||||
"Unknown Role",
|
||||
"",
|
||||
null,
|
||||
undefined,
|
||||
])("should deny viewPolicies to %s", (role) => {
|
||||
expect(hasCapability(role, "viewPolicies")).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer"])("should grant viewPrompts to %s", (role) => {
|
||||
expect(hasCapability(role, "viewPrompts")).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"Internal User",
|
||||
"Internal Viewer",
|
||||
"internal_user",
|
||||
"App User",
|
||||
"Org Admin",
|
||||
"Unknown Role",
|
||||
"",
|
||||
null,
|
||||
undefined,
|
||||
])("should deny viewPrompts to %s", (role) => {
|
||||
expect(hasCapability(role, "viewPrompts")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["viewAuditLogs", "viewDeletedTeams"] as const)("hasCapability - %s", (capability) => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ const CAPABILITY_ROLES = {
|
|||
viewToolPolicies: all_admin_roles,
|
||||
viewAuditLogs: all_admin_roles,
|
||||
viewDeletedTeams: all_admin_roles,
|
||||
viewPolicies: all_admin_roles,
|
||||
viewPrompts: all_admin_roles,
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
|
||||
export type Capability = keyof typeof CAPABILITY_ROLES;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue