mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +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
78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
import pytest
|
|
|
|
from .actors import Actor
|
|
from .conftest import create_scratch_team
|
|
|
|
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|
|
|
|
|
# POST /team/delete asks TeamAccess.allows per team. The request carries the
|
|
# team's organization_id so an org admin of that org clears the management-
|
|
# route gate; a team admin is an INTERNAL_USER on a non-internal_user route,
|
|
# so a team admin never reaches the handler. Only PROXY_ADMIN and an org admin
|
|
# of the team's own org can delete it.
|
|
_MATRIX = [
|
|
("alpha/proxy_admin", Actor.PROXY_ADMIN, "alpha", 200),
|
|
("alpha/org_admin", Actor.ORG_ADMIN, "alpha", 200),
|
|
("alpha/team_admin", Actor.TEAM_ADMIN, "alpha", 401),
|
|
("alpha/internal_user", Actor.INTERNAL_USER, "alpha", 401),
|
|
("alpha/cross_org_user", Actor.CROSS_ORG_USER, "alpha", 401),
|
|
("alpha/org_b_admin", Actor.ORG_B_ADMIN, "alpha", 401),
|
|
("beta/proxy_admin", Actor.PROXY_ADMIN, "beta", 200),
|
|
("beta/org_admin", Actor.ORG_ADMIN, "beta", 401),
|
|
("beta/org_b_admin", Actor.ORG_B_ADMIN, "beta", 200),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"actor,shape,expected_status",
|
|
[(a, sh, s) for (_id, a, sh, s) in _MATRIX],
|
|
ids=[s[0] for s in _MATRIX],
|
|
)
|
|
async def test_team_delete_authz_matrix(
|
|
actor: Actor,
|
|
shape: str,
|
|
expected_status: int,
|
|
proxy_client,
|
|
prisma,
|
|
scratch,
|
|
world,
|
|
):
|
|
org_id = world.org_a_id if shape == "alpha" else world.org_b_id
|
|
await create_scratch_team(prisma, scratch.prefix, organization_id=org_id)
|
|
caller = world.keys[actor]
|
|
|
|
resp = await proxy_client.post(
|
|
"/team/delete",
|
|
headers={"Authorization": f"Bearer {caller.cleartext}"},
|
|
json={"team_ids": [scratch.prefix], "organization_id": org_id},
|
|
)
|
|
assert (
|
|
resp.status_code == expected_status
|
|
), f"{actor.value} {shape}: {resp.status_code} {resp.text}"
|
|
|
|
row = await prisma.db.litellm_teamtable.find_unique(
|
|
where={"team_id": scratch.prefix}
|
|
)
|
|
if expected_status == 200:
|
|
assert row is None, "deleted but team row survives"
|
|
else:
|
|
assert row is not None, "denied but team row vanished"
|
|
|
|
|
|
async def test_team_delete_batch_with_missing_id_deletes_nothing(
|
|
proxy_client, prisma, scratch, world
|
|
):
|
|
"""A batch is validated whole before any deletion: one missing team_id
|
|
fails the request 404 and the accessible team in the batch survives."""
|
|
await create_scratch_team(prisma, scratch.prefix, organization_id=world.org_a_id)
|
|
resp = await proxy_client.post(
|
|
"/team/delete",
|
|
headers={"Authorization": f"Bearer {world.keys[Actor.PROXY_ADMIN].cleartext}"},
|
|
json={"team_ids": [scratch.prefix, "behavior-pin-no-such-team"]},
|
|
)
|
|
assert resp.status_code == 404, resp.text
|
|
row = await prisma.db.litellm_teamtable.find_unique(
|
|
where={"team_id": scratch.prefix}
|
|
)
|
|
assert row is not None, "batch aborted but the accessible team was deleted"
|