mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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 <krrish-berri-2@users.noreply.github.com>
This commit is contained in:
parent
7e80e094c4
commit
ca2dc46d0f
5 changed files with 49 additions and 8 deletions
|
|
@ -10,10 +10,18 @@ export interface OrganizationListFilters {
|
|||
org_alias?: string | null;
|
||||
}
|
||||
|
||||
export const useOrganizations = (filters?: OrganizationListFilters): UseQueryResult<Organization[]> => {
|
||||
export interface UseOrganizationsOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const useOrganizations = (
|
||||
filters?: OrganizationListFilters,
|
||||
options?: UseOrganizationsOptions,
|
||||
): UseQueryResult<Organization[]> => {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
const orgId = filters?.org_id || null;
|
||||
const orgAlias = filters?.org_alias || null;
|
||||
const enabled = options?.enabled ?? true;
|
||||
return useQuery<Organization[]>({
|
||||
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),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Capability>([
|
||||
"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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export type Capability = keyof typeof CAPABILITY_ROLES;
|
|||
|
||||
const ORG_ADMIN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>(["viewDeletedTeams"]);
|
||||
|
||||
export const capabilityRequiresOrgAdmin = (capability: Capability): boolean => ORG_ADMIN_CAPABILITIES.has(capability);
|
||||
|
||||
export const hasCapability = (
|
||||
userRole: string | null | undefined,
|
||||
capability: Capability,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue