Allow internal users to view tag usage

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
This commit is contained in:
oss-agent-shin 2026-05-06 03:06:41 +00:00
parent e9fb29061a
commit c95f57f095
No known key found for this signature in database
6 changed files with 89 additions and 5 deletions

View file

@ -672,12 +672,17 @@ class LiteLLMRoutes(enum.Enum):
"/v2/guardrails/list",
"/project/list",
"/project/info",
"/tag/daily/activity",
"/tag/list",
]
+ spend_tracking_routes
+ key_management_routes
)
internal_user_view_only_routes = spend_tracking_routes
internal_user_view_only_routes = spend_tracking_routes + [
"/tag/daily/activity",
"/tag/list",
]
self_managed_routes = [
"/team/member_add",

View file

@ -53,6 +53,43 @@ def test_non_admin_config_update_route_rejected():
assert "Your role=internal_user" in str(exc_info.value)
@pytest.mark.parametrize(
"role",
[
LitellmUserRoles.INTERNAL_USER.value,
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value,
],
)
@pytest.mark.parametrize("route", ["/tag/list", "/tag/daily/activity"])
def test_internal_users_can_access_tag_usage_read_routes(role, route):
"""
Internal users should be able to load the UI's tag usage view.
"""
user_obj = LiteLLM_UserTable(
user_id="internal_user",
user_email="user@example.com",
user_role=role,
)
valid_token = UserAPIKeyAuth(
user_id="internal_user",
user_role=role,
)
request = MagicMock(spec=Request)
request.query_params = {}
try:
RouteChecks.non_proxy_admin_allowed_routes_check(
user_obj=user_obj,
_user_role=role,
route=route,
request=request,
valid_token=valid_token,
request_data={},
)
except Exception as e:
pytest.fail(f"{role} should be able to access {route}. Got error: {str(e)}")
def test_proxy_admin_viewer_config_update_route_rejected():
"""Test that proxy admin viewer users are rejected when trying to call /config/update"""

View file

@ -57,7 +57,7 @@ vi.mock("./EndpointUsage/EndpointUsage", () => ({
vi.mock("./UsageViewSelect/UsageViewSelect", async () => {
const React = await import("react");
const UsageViewSelect = ({ value, onChange }: any) => {
const UsageViewSelect = ({ value, onChange, canViewTagUsage }: any) => {
return React.createElement(
"select",
{
@ -70,7 +70,7 @@ vi.mock("./UsageViewSelect/UsageViewSelect", async () => {
React.createElement("option", { value: "team" }, "Team Usage"),
React.createElement("option", { value: "organization" }, "Organization Usage"),
React.createElement("option", { value: "customer" }, "Customer Usage"),
React.createElement("option", { value: "tag" }, "Tag Usage"),
canViewTagUsage && React.createElement("option", { value: "tag" }, "Tag Usage"),
React.createElement("option", { value: "agent" }, "Agent Usage"),
React.createElement("option", { value: "user-agent-activity" }, "User Agent Activity"),
);
@ -808,6 +808,25 @@ describe("UsagePage", () => {
});
describe("non-admin user behavior", () => {
it("should render tag usage option for internal users", async () => {
mockUseAuthorized.mockReturnValue({
isLoading: false,
isAuthorized: true,
token: "mock-token",
accessToken: "test-token",
userId: "user-123",
userEmail: "test@example.com",
userRole: "Internal User",
premiumUser: false,
disabledPersonalKeyCreation: false,
showSSOBanner: false,
});
renderWithProviders(<UsagePage {...defaultProps} />);
expect(screen.getByRole("option", { name: "Tag Usage" })).toBeInTheDocument();
});
it("should not render user selector for non-admin users", async () => {
mockUseAuthorized.mockReturnValue({
isLoading: false,

View file

@ -31,7 +31,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
import { useInfiniteUsers } from "@/app/(dashboard)/hooks/users/useUsers";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { all_admin_roles } from "../../../utils/roles";
import { all_admin_roles, internalUserRoles } from "../../../utils/roles";
import { ActivityMetrics, processActivityData } from "../../activity_metrics";
import CloudZeroExportModal from "../../cloudzero_export_modal";
import EntityUsageExportModal from "../../EntityUsageExport";
@ -84,6 +84,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
console.log(`currentUser: ${JSON.stringify(currentUser)}`);
console.log(`currentUser max budget: ${currentUser?.max_budget}`);
const isAdmin = all_admin_roles.includes(userRole || "");
const canViewTagUsage = isAdmin || internalUserRoles.includes(userRole || "");
// Debounced search for user selector
const [userSearchInput, setUserSearchInput] = useState("");
@ -437,7 +438,12 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<div className="flex items-end justify-between gap-6 mb-6">
<div className="flex-1">
<div className="flex items-end justify-between gap-6 mb-4 w-full">
<UsageViewSelect value={usageView} onChange={(value) => setUsageView(value)} isAdmin={isAdmin} />
<UsageViewSelect
value={usageView}
onChange={(value) => setUsageView(value)}
isAdmin={isAdmin}
canViewTagUsage={canViewTagUsage}
/>
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} />
</div>
{paginatedResult.isFetchingMore && (

View file

@ -110,4 +110,16 @@ describe("UsageViewSelect", () => {
expect(mockOnChange).toHaveBeenCalledWith("team");
});
it("should show tag usage when tag usage access is allowed", () => {
render(<UsageViewSelect value="global" onChange={mockOnChange} isAdmin={false} canViewTagUsage={true} />);
expect(screen.getByRole("option", { name: "Tag Usage" })).toBeInTheDocument();
});
it("should hide tag usage for non-admins without tag usage access", () => {
render(<UsageViewSelect value="global" onChange={mockOnChange} isAdmin={false} />);
expect(screen.queryByRole("option", { name: "Tag Usage" })).not.toBeInTheDocument();
});
});

View file

@ -16,6 +16,7 @@ export interface UsageViewSelectProps {
value: UsageOption;
onChange: (value: UsageOption) => void;
isAdmin: boolean;
canViewTagUsage?: boolean;
title?: string;
description?: string;
"data-id"?: string;
@ -106,12 +107,16 @@ export const UsageViewSelect: React.FC<UsageViewSelectProps> = ({
value,
onChange,
isAdmin,
canViewTagUsage = false,
title = "Usage View",
description = "Select the usage data you want to view",
"data-id": dataId,
}) => {
const getFilteredOptions = () => {
return OPTIONS.filter((option) => {
if (option.value === "tag") {
return isAdmin || canViewTagUsage;
}
if (option.adminOnly && !isAdmin) {
return false;
}