mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(ui): gate policy and prompt lookups on an admin capability
/policies/list and /prompts/list are default-deny for internal_user, but the Virtual Keys create/edit flow, the Teams forms and the Playground called them on mount, so every internal user landing on the dashboard fired two requests that 401. Add viewPolicies and viewPrompts to the capability map and use them to gate the nav entry, the form field and the fetch together, following the pattern from the Tool Policies migration. Non-admins now see no policy or prompt selector at all rather than an empty dropdown.
This commit is contained in:
parent
e9d1ea59fb
commit
2502ee4a2a
16 changed files with 533 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,51 @@ 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();
|
||||
|
||||
// The modal still renders the rest of the advanced settings, so a missing
|
||||
// selector is a real omission rather than an un-rendered tree.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("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
|
||||
|
|
|
|||
|
|
@ -22,6 +22,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()),
|
||||
|
|
@ -227,6 +232,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([]);
|
||||
|
|
@ -631,6 +637,46 @@ describe("TeamInfoView", () => {
|
|||
});
|
||||
|
||||
describe("settings and editing", () => {
|
||||
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 }));
|
||||
|
||||
// `selector` skips the read-only Policies card, which renders the team's own
|
||||
// policy names from team info and needs no admin list.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Policies", { selector: "span" })).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 }));
|
||||
|
||||
// Team Name proves the edit form rendered, so a missing Policies field is a real omission.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Team Name")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(networking.getPoliciesList).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("Policies", { selector: "span" })).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";
|
||||
|
|
@ -196,6 +197,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);
|
||||
|
|
@ -293,8 +295,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
fetchPolicies();
|
||||
}, [accessToken]);
|
||||
if (canViewPolicies) fetchPolicies();
|
||||
}, [accessToken, canViewPolicies]);
|
||||
|
||||
// Fetch resolved guardrails for all policies
|
||||
useEffect(() => {
|
||||
|
|
@ -1284,30 +1286,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,17 @@ 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";
|
||||
|
||||
// PolicySelector gates itself on the session capability; in this bare template
|
||||
// render there is no session, so stub the hook to an admin to keep the selector
|
||||
// mounted and prove the gate under test is KeyEditView's own.
|
||||
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 +220,46 @@ 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");
|
||||
|
||||
// Models still loads, so the form really rendered and the fields are absent by gate.
|
||||
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 { rolesWithWriteAccess } from "../../utils/roles";
|
||||
import AgentSelector from "../agent_management/AgentSelector";
|
||||
import AccessGroupSelector from "../common_components/AccessGroupSelector";
|
||||
|
|
@ -83,6 +84,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 [form] = Form.useForm();
|
||||
const [promptsList, setPromptsList] = useState<string[]>([]);
|
||||
const [tagsList, setTagsList] = useState<Record<string, Tag>>({});
|
||||
|
|
@ -148,9 +151,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(() => {
|
||||
|
|
@ -610,27 +613,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
|
||||
|
|
@ -645,23 +650,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,34 @@ describe("hasCapability", () => {
|
|||
expect(hasCapability(role, "viewToolPolicies")).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer", "org_admin"])(
|
||||
"should grant viewPolicies to %s",
|
||||
(role) => {
|
||||
expect(hasCapability(role, "viewPolicies")).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["Internal User", "Internal Viewer", "internal_user", "App User", "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", "org_admin"])(
|
||||
"should grant viewPrompts to %s",
|
||||
(role) => {
|
||||
expect(hasCapability(role, "viewPrompts")).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["Internal User", "Internal Viewer", "internal_user", "App User", "Unknown Role", "", null, undefined])(
|
||||
"should deny viewPrompts to %s",
|
||||
(role) => {
|
||||
expect(hasCapability(role, "viewPrompts")).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("rolesWithCapability", () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { all_admin_roles } from "./roles";
|
|||
|
||||
const CAPABILITY_ROLES = {
|
||||
viewToolPolicies: 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