mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 23:05:52 +00:00
67e6e5e1df
* test(proxy): behavior-pinning matrix for team management endpoints PR2 (Team Tier-1) of the management-endpoint behavior-pinning effort. Extends the tests/proxy_behavior/management/ harness PR1 built and adds the actor x target-resource authz matrix for the 7 team endpoints: /team/new, /team/info, /team/list, /team/update, /team/member_add, /team/member_delete, /team/member_update. Tests-only, no production code changes. Harness extensions: - actors.py: ORG_B_ADMIN actor (org admin of ORG_B) and TEAM_GAMMA (an ORG_A team with no actor members), so team-targeting endpoints get a clean own / same-org-other / cross-org target axis. - conftest.py: create_scratch_team() raw-seeds target teams without /team/new side effects; the scratch teardown now also strips dangling scratch-team refs from LiteLLM_UserTable.teams. 156 new scenarios; status codes pinned to observed handler behavior. * test(proxy): record mutmut run blockers in PR2 triage doc Attempted a scoped local mutmut run for G5; it did not complete. Record the three concrete blockers in mutmut_triage/pr2-team-tier1.md so the next attempt has a head start: 1. mutmut's mutants/ sandbox is import-shadowed by the worktree source. 2. the legacy mock suite and the real-DB behavior suite cannot share a pytest session (mock suite globally patches prisma_client). 3. the CI mutation-test.yml workflow starts no Postgres, so its stats phase now aborts on the behavior-suite tests PR1 added to tests_dir. mutmut stays a deferred follow-up (as in PR1); the binding pre-merge signal remains the behavior matrix (G1) and the G4 regression-replay. * test(proxy): drop suite README + triage doc, trim test comments Remove the two prose docs from the behavior suite (README.md and mutmut_triage/pr2-team-tier1.md) and tighten the comment blocks on the team test files + harness down to the load-bearing parts (the gate each matrix pins, plus genuinely surprising results). No behavior change — all 286 scenarios still pass. * test(proxy): remove mutmut tests_dir comment
98 lines
3.4 KiB
Python
98 lines
3.4 KiB
Python
import pytest
|
|
|
|
from .actors import Actor
|
|
from .conftest import create_scratch_team
|
|
|
|
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|
|
|
|
|
# POST /team/member_update — actor x team-shape matrix. The scratch team is
|
|
# raw-seeded with a "user"-role member; each scenario tries to promote it to
|
|
# "admin". PROXY_ADMIN, the team's team admin, or an org admin of the team's
|
|
# org may update members; else 403. (The harness forces premium_user, so the
|
|
# promotion does not hit the admin-role premium gate.)
|
|
_MATRIX = [
|
|
("alpha/proxy_admin", Actor.PROXY_ADMIN, "alpha", 200),
|
|
("alpha/org_admin", Actor.ORG_ADMIN, "alpha", 200),
|
|
("alpha/team_admin", Actor.TEAM_ADMIN, "alpha", 200),
|
|
("alpha/internal_user", Actor.INTERNAL_USER, "alpha", 403),
|
|
("alpha/owner", Actor.OWNER, "alpha", 403),
|
|
("alpha/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "alpha", 403),
|
|
("alpha/cross_org_user", Actor.CROSS_ORG_USER, "alpha", 403),
|
|
("alpha/service_account", Actor.SERVICE_ACCOUNT, "alpha", 403),
|
|
("alpha/org_b_admin", Actor.ORG_B_ADMIN, "alpha", 403),
|
|
("beta/proxy_admin", Actor.PROXY_ADMIN, "beta", 200),
|
|
("beta/org_admin", Actor.ORG_ADMIN, "beta", 403),
|
|
("beta/team_admin", Actor.TEAM_ADMIN, "beta", 403),
|
|
("beta/internal_user", Actor.INTERNAL_USER, "beta", 403),
|
|
("beta/owner", Actor.OWNER, "beta", 403),
|
|
("beta/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "beta", 403),
|
|
("beta/cross_org_user", Actor.CROSS_ORG_USER, "beta", 403),
|
|
("beta/service_account", Actor.SERVICE_ACCOUNT, "beta", 403),
|
|
("beta/org_b_admin", Actor.ORG_B_ADMIN, "beta", 200),
|
|
]
|
|
|
|
|
|
async def _seed_target(prisma, world, shape: str, team_id: str, member_id: str) -> None:
|
|
if shape == "alpha":
|
|
await create_scratch_team(
|
|
prisma,
|
|
team_id,
|
|
organization_id=world.org_a_id,
|
|
admin_user_ids=[world.keys[Actor.TEAM_ADMIN].user_id],
|
|
member_user_ids=[member_id],
|
|
)
|
|
elif shape == "beta":
|
|
await create_scratch_team(
|
|
prisma,
|
|
team_id,
|
|
organization_id=world.org_b_id,
|
|
member_user_ids=[member_id],
|
|
)
|
|
else: # pragma: no cover - guard
|
|
pytest.fail(f"unknown shape={shape}")
|
|
|
|
|
|
def _role_of(row, user_id: str):
|
|
for m in row.members_with_roles or []:
|
|
if m["user_id"] == user_id:
|
|
return m["role"]
|
|
return None
|
|
|
|
|
|
@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_member_update_authz_matrix(
|
|
actor: Actor,
|
|
shape: str,
|
|
expected_status: int,
|
|
proxy_client,
|
|
prisma,
|
|
scratch,
|
|
world,
|
|
):
|
|
member_id = scratch.tag("member")
|
|
await _seed_target(prisma, world, shape, scratch.prefix, member_id)
|
|
caller = world.keys[actor]
|
|
|
|
resp = await proxy_client.post(
|
|
"/team/member_update",
|
|
headers={"Authorization": f"Bearer {caller.cleartext}"},
|
|
json={"team_id": scratch.prefix, "user_id": member_id, "role": "admin"},
|
|
)
|
|
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}
|
|
)
|
|
assert row is not None
|
|
if expected_status == 200:
|
|
assert _role_of(row, member_id) == "admin"
|
|
else:
|
|
assert _role_of(row, member_id) == "user", "denied but role changed"
|