From ca2dc46d0f582dd4220a0bddd8000558354fd446 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 05:34:29 +0000 Subject: [PATCH] fix(ui): skip org-list fetch in useCan when capability cannot open for org admins useCan started calling useIsOrgAdmin unconditionally in PR #36478 so it could opt viewDeletedTeams open for organization admins, but useIsOrgAdmin drives a useOrganizations query, so every role-gated page now fires GET /organization/list just to compute a denial. The Memory, Workflows and Guardrails Monitor page tests catch this: they set up a non-admin session, expect the admin-only notice, and assert fetch was never called; instead the organization list is requested and the assertion fails 12 times. Track which capabilities actually opt org admins in (only viewDeletedTeams today) via capabilityRequiresOrgAdmin, thread an enabled flag from useCan through useIsOrgAdmin into useOrganizations, and treat the org-admin membership lookup as false while disabled so hasCapability sees no false positive. The existing role gates for viewMemory, viewWorkflowRuns, viewGuardrailUsage, viewProxyWideCostData never look at isOrgAdmin, so this is a pure fetch skip. Co-authored-by: Krrish Dholakia --- .../hooks/organizations/useOrganizations.ts | 12 ++++++-- .../src/app/(dashboard)/hooks/useCan.ts | 4 +-- .../app/(dashboard)/hooks/useIsOrgAdmin.ts | 11 ++++++-- .../src/utils/capabilities.test.ts | 28 ++++++++++++++++++- .../src/utils/capabilities.ts | 2 ++ 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts index 734c1986f8f..7e53a86496c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts @@ -10,10 +10,18 @@ export interface OrganizationListFilters { org_alias?: string | null; } -export const useOrganizations = (filters?: OrganizationListFilters): UseQueryResult => { +export interface UseOrganizationsOptions { + enabled?: boolean; +} + +export const useOrganizations = ( + filters?: OrganizationListFilters, + options?: UseOrganizationsOptions, +): UseQueryResult => { const { accessToken, userId, userRole } = useAuthorized(); const orgId = filters?.org_id || null; const orgAlias = filters?.org_alias || null; + const enabled = options?.enabled ?? true; return useQuery({ queryKey: organizationKeys.list( orgId || orgAlias @@ -21,7 +29,7 @@ export const useOrganizations = (filters?: OrganizationListFilters): UseQueryRes : {}, ), queryFn: async () => await organizationListCall(accessToken!, orgId, orgAlias), - enabled: Boolean(accessToken && userId && userRole), + enabled: enabled && Boolean(accessToken && userId && userRole), }); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts index 13903007cae..3d7788432b8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts @@ -1,13 +1,13 @@ "use client"; -import { hasCapability, type Capability } from "@/utils/capabilities"; +import { capabilityRequiresOrgAdmin, hasCapability, type Capability } from "@/utils/capabilities"; import useAuthorized from "./useAuthorized"; import useIsOrgAdmin from "./useIsOrgAdmin"; const useCan = (capability: Capability): boolean => { const { userRole } = useAuthorized(); - const isOrgAdmin = useIsOrgAdmin(); + const isOrgAdmin = useIsOrgAdmin({ enabled: capabilityRequiresOrgAdmin(capability) }); return hasCapability(userRole, capability, isOrgAdmin); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts index d93b57a3fc3..41d65ac7167 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts @@ -5,10 +5,15 @@ import { isOrgAdminForAnyOrg, isOrgAdminSessionRole } from "@/utils/roles"; import { useOrganizations } from "./organizations/useOrganizations"; import useAuthorized from "./useAuthorized"; -const useIsOrgAdmin = (): boolean => { +export interface UseIsOrgAdminOptions { + enabled?: boolean; +} + +const useIsOrgAdmin = (options?: UseIsOrgAdminOptions): boolean => { const { userId, userRole } = useAuthorized(); - const { data: organizations } = useOrganizations(); - return isOrgAdminSessionRole(userRole) || isOrgAdminForAnyOrg(organizations, userId); + const enabled = options?.enabled ?? true; + const { data: organizations } = useOrganizations(undefined, { enabled }); + return isOrgAdminSessionRole(userRole) || (enabled && isOrgAdminForAnyOrg(organizations, userId)); }; export default useIsOrgAdmin; diff --git a/ui/litellm-dashboard/src/utils/capabilities.test.ts b/ui/litellm-dashboard/src/utils/capabilities.test.ts index 3f5d4b4efbe..78f6a3221e0 100644 --- a/ui/litellm-dashboard/src/utils/capabilities.test.ts +++ b/ui/litellm-dashboard/src/utils/capabilities.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { hasCapability, rolesWithCapability, type Capability } from "./capabilities"; +import { capabilityRequiresOrgAdmin, hasCapability, rolesWithCapability, type Capability } from "./capabilities"; import { effectiveSessionRole } from "./roles"; const ADMIN_ROLES = ["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer"]; @@ -141,3 +141,29 @@ describe("rolesWithCapability", () => { expect(hasCapability(removed, "viewToolPolicies")).toBe(true); }); }); + +// Callers use this to skip the organization-list fetch that backs the org-admin +// check when the capability cannot be reopened by an org-admin membership. That +// keeps role-gated pages like Memory / Workflows / Guardrails Monitor from +// firing a network request just to compute a denial. +describe("capabilityRequiresOrgAdmin", () => { + it("is true only for viewDeletedTeams, the sole capability that opts org admins in", () => { + expect(capabilityRequiresOrgAdmin("viewDeletedTeams")).toBe(true); + }); + + it.each([ + "viewToolPolicies", + "viewAuditLogs", + "viewPolicies", + "viewPrompts", + "viewOrganizationUsage", + "viewAgentUsage", + "viewGlobalSpend", + "viewWorkflowRuns", + "viewMemory", + "viewGuardrailUsage", + "viewProxyWideCostData", + ])("is false for %s so useCan can skip the org-list fetch for it", (capability) => { + expect(capabilityRequiresOrgAdmin(capability)).toBe(false); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/capabilities.ts b/ui/litellm-dashboard/src/utils/capabilities.ts index dabbc827bcb..abb130bd255 100644 --- a/ui/litellm-dashboard/src/utils/capabilities.ts +++ b/ui/litellm-dashboard/src/utils/capabilities.ts @@ -21,6 +21,8 @@ export type Capability = keyof typeof CAPABILITY_ROLES; const ORG_ADMIN_CAPABILITIES: ReadonlySet = new Set(["viewDeletedTeams"]); +export const capabilityRequiresOrgAdmin = (capability: Capability): boolean => ORG_ADMIN_CAPABILITIES.has(capability); + export const hasCapability = ( userRole: string | null | undefined, capability: Capability,