mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
* refactor(proxy): route every team-admin decision through auth/team_access.py Move the six team-admin helpers out of common_utils, team_endpoints and key_management_endpoints into litellm/proxy/auth/team_access.py under public names, and point every management route and helper at them. The key routes keep checking team admin before org admin, so a team admin whose user row is gone still passes as before. Status codes and bodies are unchanged, which the 223-case team-admin matrix confirms at the merge base and at the tip common_utils keeps `_is_user_team_admin` as an alias because the published litellm-enterprise 0.1.71 wheel still imports it from there * refactor(proxy): answer every team access check with TeamAccess.allows Replace the six helpers in auth/team_access.py with one resolver in litellm/proxy/management/teams/access.py. Each route passes the roles it accepts (TEAM_OR_ORG_ADMIN or TEAM_ADMIN_ONLY), and /team/update and /team/info rank roles through strongest_role so org admin still outranks team admin there The org lookup moves behind an OrgRoles protocol, implemented by PrismaOrgRoles in management/users/service.py, and get_team_access in management/teams/dependencies.py is the only place that reads proxy_server globals. _check_key_admin_access keeps its name and body from main Routes that checked org admin first now read the roster first, so a team admin whose org lookup errors now passes on /team/delete, /team/block, /team/unblock, member reset_spend and reset_budget, and the team callback routes. No allowed caller is denied
53 lines
2.1 KiB
Python
53 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
from litellm.caching.dual_cache import DualCache
|
|
from litellm.proxy._types import LiteLLM_OrganizationMembershipTable, LiteLLM_UserTable, LitellmUserRoles
|
|
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
|
from litellm.proxy.management.users.service import PrismaOrgRoles, holds_org_admin
|
|
from litellm.proxy.utils import ProxyLogging
|
|
|
|
NOW: Final = datetime.now(timezone.utc)
|
|
|
|
|
|
def user_in(*memberships: tuple[str, str]) -> LiteLLM_UserTable:
|
|
return LiteLLM_UserTable(
|
|
user_id="u1",
|
|
organization_memberships=[
|
|
LiteLLM_OrganizationMembershipTable(
|
|
user_id="u1", organization_id=organization_id, user_role=role, created_at=NOW, updated_at=NOW
|
|
)
|
|
for organization_id, role in memberships
|
|
],
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("user", "expected"),
|
|
[
|
|
(user_in(("org-1", LitellmUserRoles.ORG_ADMIN.value)), True),
|
|
(user_in(("org-2", LitellmUserRoles.ORG_ADMIN.value)), False),
|
|
(user_in(("org-1", LitellmUserRoles.INTERNAL_USER.value)), False),
|
|
(user_in(("org-2", LitellmUserRoles.ORG_ADMIN.value), ("org-1", LitellmUserRoles.ORG_ADMIN.value)), True),
|
|
(user_in(), False),
|
|
(LiteLLM_UserTable(user_id="u1", organization_memberships=None), False),
|
|
(None, False),
|
|
],
|
|
)
|
|
def test_holds_org_admin_needs_the_org_admin_role_in_that_org(user: LiteLLM_UserTable | None, expected: bool) -> None:
|
|
assert holds_org_admin(user, "org-1") is expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("organization_id", "expected"),
|
|
[("org-1", True), ("org-2", False)],
|
|
)
|
|
async def test_prisma_org_roles_answers_from_the_cached_user_row(organization_id: str, expected: bool) -> None:
|
|
cache: Final = UserApiKeyCache()
|
|
await cache.async_set_cache(key="u1", value=user_in(("org-1", LitellmUserRoles.ORG_ADMIN.value)))
|
|
roles: Final = PrismaOrgRoles(None, cache, ProxyLogging(user_api_key_cache=DualCache()))
|
|
assert await roles.is_org_admin("u1", organization_id) is expected
|