From 9247826cddee09bcdc6f82ecf039c39a31224009 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 5 Oct 2026 12:11:20 -0700 Subject: [PATCH] refactor(proxy): rename management/teams/access.py to authz.py (#44624) access already means access groups in this codebase; authz names what the module decides (who may act on a team). Pure rename: every importer now uses authz, and access.py stays as a re-export because the published litellm-enterprise 0.1.73 wheel imports is_team_admin from it. --- .../management_endpoints/project_endpoints.py | 2 +- litellm/proxy/auth/authorization.py | 2 +- litellm/proxy/management/teams/access.py | 73 +++++-------------- litellm/proxy/management/teams/authz.py | 55 ++++++++++++++ .../proxy/management/teams/dependencies.py | 2 +- .../auto_router_endpoints.py | 2 +- .../management_endpoints/common_utils.py | 2 +- .../internal_user_endpoints.py | 2 +- .../key_management_endpoints.py | 2 +- .../model_management_endpoints.py | 2 +- .../team_callback_endpoints.py | 2 +- .../management_endpoints/team_endpoints.py | 2 +- .../bulk_team_member_budgets.py | 2 +- .../management_helpers/bulk_user_creation.py | 2 +- .../management_helpers/bulk_user_deletion.py | 2 +- litellm/proxy/memory/memory_endpoints.py | 2 +- .../teams/{test_access.py => test_authz.py} | 9 ++- .../test_team_callback_endpoints.py | 2 +- .../test_team_endpoints.py | 2 +- .../test_spend_management_endpoints.py | 2 +- 20 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 litellm/proxy/management/teams/authz.py rename tests/unit/proxy/management/teams/{test_access.py => test_authz.py} (94%) diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py index d134c39c91b..313867bbec4 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py @@ -22,7 +22,7 @@ from litellm._uuid import uuid from litellm.proxy._types import * from litellm.proxy.auth.auth_checks import delete_cached_project_object from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.management.teams.access import is_team_admin +from litellm.proxy.management.teams.authz import is_team_admin from litellm.proxy.management_endpoints.common_utils import _set_object_metadata_field from litellm.proxy.management_endpoints.team_admin_field_permissions import team_admin_may_manage_projects from litellm.proxy.management_helpers.utils import ( diff --git a/litellm/proxy/auth/authorization.py b/litellm/proxy/auth/authorization.py index 91549f19d2e..cd0a7acff0a 100644 --- a/litellm/proxy/auth/authorization.py +++ b/litellm/proxy/auth/authorization.py @@ -36,7 +36,7 @@ async def resolve_owned_read_scope( def can_read_team_logs(auth: UserAPIKeyAuth, team: LiteLLM_TeamTable) -> bool: - from litellm.proxy.management.teams.access import is_team_admin + from litellm.proxy.management.teams.authz import is_team_admin from litellm.proxy.management_endpoints.common_utils import ( _team_member_has_permission, # pyright: ignore[reportPrivateUsage] # reuse existing team permission policy ) diff --git a/litellm/proxy/management/teams/access.py b/litellm/proxy/management/teams/access.py index 77af588c636..293ab5bb747 100644 --- a/litellm/proxy/management/teams/access.py +++ b/litellm/proxy/management/teams/access.py @@ -1,55 +1,22 @@ -"""Who may act on a team: every management route asks ``TeamAccess.allows`` with the roles it accepts.""" +"""Moved to ``authz``. Kept because the published litellm-enterprise 0.1.73 wheel imports ``is_team_admin`` from here; +delete once the enterprise pin moves to a release that imports from ``authz``.""" -from __future__ import annotations +from litellm.proxy.management.teams.authz import ( + TEAM_ADMIN_ONLY, + TEAM_OR_ORG_ADMIN, + OrgRoles, + TeamAccess, + TeamRole, + is_team_admin, + team_access_denied, +) -from dataclasses import dataclass -from typing import Final, Literal, NoReturn, Protocol, TypeAlias - -from fastapi import HTTPException, status - -from litellm.proxy._types import LiteLLM_TeamTable, LitellmUserRoles, UserAPIKeyAuth - -TeamRole: TypeAlias = Literal["proxy_admin", "org_admin", "team_admin"] -TEAM_ADMIN_ONLY: Final[frozenset[TeamRole]] = frozenset({"proxy_admin", "team_admin"}) -TEAM_OR_ORG_ADMIN: Final[frozenset[TeamRole]] = frozenset({"proxy_admin", "team_admin", "org_admin"}) - - -class OrgRoles(Protocol): - async def is_org_admin(self, user_id: str, organization_id: str) -> bool: ... - - -@dataclass(frozen=True, slots=True) -class TeamAccess: - org_roles: OrgRoles - - async def allows(self, caller: UserAPIKeyAuth, team: LiteLLM_TeamTable, allow: frozenset[TeamRole]) -> bool: - """Team admin is checked before org admin, so only callers off the roster pay for the org lookup.""" - if "proxy_admin" in allow and caller.user_role == LitellmUserRoles.PROXY_ADMIN: - return True - if "team_admin" in allow and is_team_admin(caller, team): - return True - return "org_admin" in allow and await self._is_org_admin(caller, team) - - async def strongest_role(self, caller: UserAPIKeyAuth, team: LiteLLM_TeamTable) -> TeamRole | None: - """Org admin outranks team admin so a caller holding both keeps unrestricted edits.""" - if caller.user_role == LitellmUserRoles.PROXY_ADMIN: - return "proxy_admin" - if await self._is_org_admin(caller, team): - return "org_admin" - return "team_admin" if is_team_admin(caller, team) else None - - async def _is_org_admin(self, caller: UserAPIKeyAuth, team: LiteLLM_TeamTable) -> bool: - if not caller.user_id or not team.organization_id: - return False - return await self.org_roles.is_org_admin(caller.user_id, team.organization_id) - - -def is_team_admin(user_api_key_dict: UserAPIKeyAuth, team_obj: LiteLLM_TeamTable) -> bool: - return any( - member.user_id is not None and member.user_id == user_api_key_dict.user_id and member.role == "admin" - for member in team_obj.members_with_roles - ) - - -def team_access_denied() -> NoReturn: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have access to this team") +__all__ = [ + "TEAM_ADMIN_ONLY", + "TEAM_OR_ORG_ADMIN", + "OrgRoles", + "TeamAccess", + "TeamRole", + "is_team_admin", + "team_access_denied", +] diff --git a/litellm/proxy/management/teams/authz.py b/litellm/proxy/management/teams/authz.py new file mode 100644 index 00000000000..77af588c636 --- /dev/null +++ b/litellm/proxy/management/teams/authz.py @@ -0,0 +1,55 @@ +"""Who may act on a team: every management route asks ``TeamAccess.allows`` with the roles it accepts.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final, Literal, NoReturn, Protocol, TypeAlias + +from fastapi import HTTPException, status + +from litellm.proxy._types import LiteLLM_TeamTable, LitellmUserRoles, UserAPIKeyAuth + +TeamRole: TypeAlias = Literal["proxy_admin", "org_admin", "team_admin"] +TEAM_ADMIN_ONLY: Final[frozenset[TeamRole]] = frozenset({"proxy_admin", "team_admin"}) +TEAM_OR_ORG_ADMIN: Final[frozenset[TeamRole]] = frozenset({"proxy_admin", "team_admin", "org_admin"}) + + +class OrgRoles(Protocol): + async def is_org_admin(self, user_id: str, organization_id: str) -> bool: ... + + +@dataclass(frozen=True, slots=True) +class TeamAccess: + org_roles: OrgRoles + + async def allows(self, caller: UserAPIKeyAuth, team: LiteLLM_TeamTable, allow: frozenset[TeamRole]) -> bool: + """Team admin is checked before org admin, so only callers off the roster pay for the org lookup.""" + if "proxy_admin" in allow and caller.user_role == LitellmUserRoles.PROXY_ADMIN: + return True + if "team_admin" in allow and is_team_admin(caller, team): + return True + return "org_admin" in allow and await self._is_org_admin(caller, team) + + async def strongest_role(self, caller: UserAPIKeyAuth, team: LiteLLM_TeamTable) -> TeamRole | None: + """Org admin outranks team admin so a caller holding both keeps unrestricted edits.""" + if caller.user_role == LitellmUserRoles.PROXY_ADMIN: + return "proxy_admin" + if await self._is_org_admin(caller, team): + return "org_admin" + return "team_admin" if is_team_admin(caller, team) else None + + async def _is_org_admin(self, caller: UserAPIKeyAuth, team: LiteLLM_TeamTable) -> bool: + if not caller.user_id or not team.organization_id: + return False + return await self.org_roles.is_org_admin(caller.user_id, team.organization_id) + + +def is_team_admin(user_api_key_dict: UserAPIKeyAuth, team_obj: LiteLLM_TeamTable) -> bool: + return any( + member.user_id is not None and member.user_id == user_api_key_dict.user_id and member.role == "admin" + for member in team_obj.members_with_roles + ) + + +def team_access_denied() -> NoReturn: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have access to this team") diff --git a/litellm/proxy/management/teams/dependencies.py b/litellm/proxy/management/teams/dependencies.py index d3be5c6791e..ba35cdde28e 100644 --- a/litellm/proxy/management/teams/dependencies.py +++ b/litellm/proxy/management/teams/dependencies.py @@ -1,6 +1,6 @@ from __future__ import annotations -from litellm.proxy.management.teams.access import TeamAccess +from litellm.proxy.management.teams.authz import TeamAccess from litellm.proxy.management.users.service import PrismaOrgRoles diff --git a/litellm/proxy/management_endpoints/auto_router_endpoints.py b/litellm/proxy/management_endpoints/auto_router_endpoints.py index 5db0fce7d92..e372ccedd80 100644 --- a/litellm/proxy/management_endpoints/auto_router_endpoints.py +++ b/litellm/proxy/management_endpoints/auto_router_endpoints.py @@ -43,7 +43,7 @@ from litellm.proxy.litellm_pre_call_utils import ( LiteLLMProxyRequestSetup, refresh_proxy_server_request_body_snapshot, ) -from litellm.proxy.management.teams.access import is_team_admin +from litellm.proxy.management.teams.authz import is_team_admin from litellm.proxy.management_endpoints.common_daily_activity import daily_activity_scope from litellm.proxy.management_helpers.auto_router_permissions import ( authorize_member_auto_router_dependencies, diff --git a/litellm/proxy/management_endpoints/common_utils.py b/litellm/proxy/management_endpoints/common_utils.py index 2e29eb5fca0..be2423fb596 100644 --- a/litellm/proxy/management_endpoints/common_utils.py +++ b/litellm/proxy/management_endpoints/common_utils.py @@ -61,7 +61,7 @@ from litellm.proxy._types import ( # noqa: F401 re-exported user_api_key_has_admin_view as _user_has_admin_view, ) from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time -from litellm.proxy.management.teams.access import is_team_admin +from litellm.proxy.management.teams.authz import is_team_admin from litellm.proxy.utils import _premium_user_check from litellm.repositories.team_repository import TeamRepository from litellm.types.utils import BudgetConfig diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index d0b3a08bc77..53a5b8de30a 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -53,7 +53,7 @@ from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks from litellm.proxy.hooks.model_max_budget_limiter import build_model_max_budget_usage from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks -from litellm.proxy.management.teams.access import is_team_admin +from litellm.proxy.management.teams.authz import is_team_admin from litellm.proxy.management_endpoints.common_daily_activity import ( DailySpendRecord, ScopeDenied, diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 037b9082cd4..11810f4bd52 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -86,7 +86,7 @@ from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time from litellm.proxy.common_utils.user_api_key_cache import AUTH_OBJECTS_TARGET, UserApiKeyCache from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks from litellm.proxy.hooks.model_max_budget_limiter import build_model_max_budget_usage -from litellm.proxy.management.teams.access import TEAM_ADMIN_ONLY, TEAM_OR_ORG_ADMIN, is_team_admin +from litellm.proxy.management.teams.authz import TEAM_ADMIN_ONLY, TEAM_OR_ORG_ADMIN, is_team_admin from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.common_utils import ( _check_disable_global_guardrails_caller_permission, diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 7cc9dd06620..b0653d83f15 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -75,7 +75,7 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import ( ) from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache from litellm.proxy.db.routing_prisma_wrapper import WriterPinnedClient -from litellm.proxy.management.teams.access import TEAM_ADMIN_ONLY, is_team_admin +from litellm.proxy.management.teams.authz import TEAM_ADMIN_ONLY, is_team_admin from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.team_endpoints import ( _refresh_cached_team, diff --git a/litellm/proxy/management_endpoints/team_callback_endpoints.py b/litellm/proxy/management_endpoints/team_callback_endpoints.py index ac6169d25bd..bc15d4a4434 100644 --- a/litellm/proxy/management_endpoints/team_callback_endpoints.py +++ b/litellm/proxy/management_endpoints/team_callback_endpoints.py @@ -44,7 +44,7 @@ from litellm.proxy.litellm_pre_call_utils import ( _get_validated_callback_metadata, convert_key_logging_metadata_to_callback, ) -from litellm.proxy.management.teams.access import TEAM_OR_ORG_ADMIN, team_access_denied +from litellm.proxy.management.teams.authz import TEAM_OR_ORG_ADMIN, team_access_denied from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.team_endpoints import _refresh_cached_team from litellm.proxy.management_helpers.utils import management_endpoint_wrapper diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index e452afccef9..133df8f9b3d 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -123,7 +123,7 @@ from litellm.proxy.hooks.model_max_budget_limiter import ( build_model_max_budget_usage, resolve_model_budget, ) -from litellm.proxy.management.teams.access import TEAM_OR_ORG_ADMIN, TeamRole, is_team_admin, team_access_denied +from litellm.proxy.management.teams.authz import TEAM_OR_ORG_ADMIN, TeamRole, is_team_admin, team_access_denied from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.common_daily_activity import ( InvalidDateRange, diff --git a/litellm/proxy/management_helpers/bulk_team_member_budgets.py b/litellm/proxy/management_helpers/bulk_team_member_budgets.py index edc55ff61f9..6821e0ef413 100644 --- a/litellm/proxy/management_helpers/bulk_team_member_budgets.py +++ b/litellm/proxy/management_helpers/bulk_team_member_budgets.py @@ -23,7 +23,7 @@ from litellm.proxy._types import ( from litellm.proxy.auth.auth_checks import invalidate_team_member_spend_state from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache from litellm.proxy.db.routing_prisma_wrapper import WriterPinnedClient -from litellm.proxy.management.teams.access import TEAM_OR_ORG_ADMIN +from litellm.proxy.management.teams.authz import TEAM_OR_ORG_ADMIN from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.common_utils import ( _upsert_budget_and_membership, # pyright: ignore[reportPrivateUsage] # the single-member write, shared so the two surfaces cannot drift diff --git a/litellm/proxy/management_helpers/bulk_user_creation.py b/litellm/proxy/management_helpers/bulk_user_creation.py index 9636acb4e1e..21c0f03605b 100644 --- a/litellm/proxy/management_helpers/bulk_user_creation.py +++ b/litellm/proxy/management_helpers/bulk_user_creation.py @@ -34,7 +34,7 @@ from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks from litellm.proxy.list_api.common import PROBLEM_TYPE_BASE, ManagementProblem -from litellm.proxy.management.teams.access import TEAM_OR_ORG_ADMIN +from litellm.proxy.management.teams.authz import TEAM_OR_ORG_ADMIN from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.common_utils import validate_budget_duration from litellm.proxy.management_endpoints.internal_user_endpoints import ( diff --git a/litellm/proxy/management_helpers/bulk_user_deletion.py b/litellm/proxy/management_helpers/bulk_user_deletion.py index 8286605e16a..b4e4afcbc2a 100644 --- a/litellm/proxy/management_helpers/bulk_user_deletion.py +++ b/litellm/proxy/management_helpers/bulk_user_deletion.py @@ -34,7 +34,7 @@ from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks from litellm.proxy.list_api.common import PROBLEM_TYPE_BASE, ManagementProblem -from litellm.proxy.management.teams.access import TEAM_OR_ORG_ADMIN +from litellm.proxy.management.teams.authz import TEAM_OR_ORG_ADMIN from litellm.proxy.management.teams.dependencies import get_team_access from litellm.proxy.management_endpoints.key_management_endpoints import ( _persist_deleted_verification_tokens, # pyright: ignore[reportPrivateUsage] # same audit path /key/delete uses diff --git a/litellm/proxy/memory/memory_endpoints.py b/litellm/proxy/memory/memory_endpoints.py index 92ccdd389a7..34c80bcdce3 100644 --- a/litellm/proxy/memory/memory_endpoints.py +++ b/litellm/proxy/memory/memory_endpoints.py @@ -32,7 +32,7 @@ from litellm.proxy._types import ( user_api_key_has_admin_view, ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.management.teams.access import TEAM_OR_ORG_ADMIN +from litellm.proxy.management.teams.authz import TEAM_OR_ORG_ADMIN from litellm.proxy.management.teams.dependencies import get_team_access from litellm.repositories.prisma_protocols import TableActions from litellm.repositories.table_repositories import MemoryRepository diff --git a/tests/unit/proxy/management/teams/test_access.py b/tests/unit/proxy/management/teams/test_authz.py similarity index 94% rename from tests/unit/proxy/management/teams/test_access.py rename to tests/unit/proxy/management/teams/test_authz.py index 019be7afaa5..7f1107956be 100644 --- a/tests/unit/proxy/management/teams/test_access.py +++ b/tests/unit/proxy/management/teams/test_authz.py @@ -7,7 +7,7 @@ import pytest from fastapi import HTTPException from litellm.proxy._types import LiteLLM_TeamTable, LitellmUserRoles, Member, UserAPIKeyAuth -from litellm.proxy.management.teams.access import ( +from litellm.proxy.management.teams.authz import ( TEAM_ADMIN_ONLY, TEAM_OR_ORG_ADMIN, TeamAccess, @@ -134,3 +134,10 @@ def test_team_access_denied_is_the_403_management_routes_have_always_raised() -> team_access_denied() assert denied.value.status_code == 403 assert denied.value.detail == "You do not have access to this team" + + +def test_the_old_access_module_still_serves_the_published_enterprise_wheel() -> None: + from litellm.proxy.management.teams import access, authz + + assert access.is_team_admin is authz.is_team_admin + assert set(access.__all__) <= set(dir(authz)) diff --git a/tests/unit/proxy/management_endpoints/test_team_callback_endpoints.py b/tests/unit/proxy/management_endpoints/test_team_callback_endpoints.py index e368a26155b..331e3a7983c 100644 --- a/tests/unit/proxy/management_endpoints/test_team_callback_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_team_callback_endpoints.py @@ -21,7 +21,7 @@ from litellm.proxy._types import ( UserAPIKeyAuth, ) from litellm.proxy.common_utils.callback_config_validation import cross_entry_family_error -from litellm.proxy.management.teams.access import TeamAccess +from litellm.proxy.management.teams.authz import TeamAccess from litellm.proxy.management_endpoints.team_callback_endpoints import ( add_team_callbacks, delete_team_callback, diff --git a/tests/unit/proxy/management_endpoints/test_team_endpoints.py b/tests/unit/proxy/management_endpoints/test_team_endpoints.py index 3e71cc70099..0c71ee72f2b 100644 --- a/tests/unit/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_team_endpoints.py @@ -40,7 +40,7 @@ from litellm.proxy._types import ( UpdateTeamRequest, UserAPIKeyAuth, # Import UserAPIKeyAuth ) -from litellm.proxy.management.teams.access import TeamAccess +from litellm.proxy.management.teams.authz import TeamAccess from litellm.proxy.management_endpoints.team_endpoints import ( _STRIP_DELETED_TEAM_FROM_USERS_SQL, GetTeamMemberPermissionsResponse, diff --git a/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py index c27ad7ba0bf..83c3441dc17 100644 --- a/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py @@ -274,7 +274,7 @@ from litellm.proxy._types import ( UserAPIKeyAuth, ) from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger -from litellm.proxy.management.teams import access as team_access +from litellm.proxy.management.teams import authz as team_access from litellm.proxy.proxy_server import app from litellm.proxy.spend_tracking import spend_management_endpoints from litellm.router import Router