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,