feat(team): carry team_alias on member add, delete and role-change audit payloads

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-18 23:28:39 +00:00
parent 2a7dcc77b2
commit 209eba6718
2 changed files with 31 additions and 7 deletions

View file

@ -3150,7 +3150,7 @@ def _validate_member_user_id_provisioning(
)
def _members_audit_value(members: Sequence[Member]) -> str:
def _members_audit_value(team_alias: str | None, members: Sequence[Member]) -> str:
"""Serialize a team's member list for an audit-log value.
The audit-log columns hold a JSON object, so the member list is nested
@ -3158,13 +3158,15 @@ def _members_audit_value(members: Sequence[Member]) -> str:
"""
return safe_dumps(
{ # mutable-ok: the audit-log JSON column rejects a top-level array, so this value must be an object
"members_with_roles": tuple(member.model_dump() for member in members)
"team_alias": team_alias,
"members_with_roles": tuple(member.model_dump() for member in members),
}
)
async def _create_team_membership_audit_log(
team_id: str,
team_alias: str | None,
before_members: Sequence[Member],
after_members: Sequence[Member],
user_api_key_dict: UserAPIKeyAuth,
@ -3179,13 +3181,14 @@ async def _create_team_membership_audit_log(
user_api_key_dict=user_api_key_dict,
litellm_proxy_admin_name=litellm_proxy_admin_name,
table_name=LitellmTableNames.TEAM_TABLE_NAME,
before_value=_members_audit_value(before_members),
after_value=_members_audit_value(after_members),
before_value=_members_audit_value(team_alias, before_members),
after_value=_members_audit_value(team_alias, after_members),
)
async def _create_team_member_add_audit_logs(
team_id: str,
team_alias: str | None,
updated_users: Sequence[LiteLLM_UserTable],
existing_user_ids: frozenset[str],
before_members: Sequence[Member],
@ -3217,6 +3220,7 @@ async def _create_team_member_add_audit_logs(
membership_entry: Final = _create_team_membership_audit_log(
team_id=team_id,
team_alias=team_alias,
before_members=before_members,
after_members=after_members,
user_api_key_dict=user_api_key_dict,
@ -3460,6 +3464,7 @@ async def team_member_add(
await _create_team_member_add_audit_logs(
team_id=data.team_id,
team_alias=complete_team_data.team_alias,
updated_users=updated_users,
existing_user_ids=pre_existing_user_ids,
before_members=members_before_add,
@ -3538,6 +3543,7 @@ async def team_member_delete(
if before_members != after_members:
await _create_team_membership_audit_log(
team_id=existing_team_row.team_id,
team_alias=existing_team_row.team_alias,
before_members=before_members,
after_members=after_members,
user_api_key_dict=user_api_key_dict,
@ -3879,6 +3885,7 @@ async def team_member_update(
if members_before_role_update != tuple(team_members):
await _create_team_membership_audit_log(
team_id=data.team_id,
team_alias=team_table.team_alias,
before_members=members_before_role_update,
after_members=team_members,
user_api_key_dict=user_api_key_dict,

View file

@ -13245,9 +13245,12 @@ def test_members_audit_value_serializes_to_a_json_object():
"""The audit-log columns hold a JSON object; a top-level array is rejected by the DB."""
from litellm.proxy.management_endpoints.team_endpoints import _members_audit_value
payload = json.loads(_members_audit_value([Member(user_id="u1", role="admin"), Member(user_id="u2", role="user")]))
payload = json.loads(
_members_audit_value("my-team", [Member(user_id="u1", role="admin"), Member(user_id="u2", role="user")])
)
assert isinstance(payload, dict)
assert payload["team_alias"] == "my-team"
assert [m["user_id"] for m in payload["members_with_roles"]] == ["u1", "u2"]
@ -13272,7 +13275,7 @@ async def test_team_member_add_audits_a_user_created_from_a_list_payload(monkeyp
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
monkeypatch.setattr("litellm.proxy.proxy_server.litellm_proxy_admin_name", "default_user_id")
team_row = LiteLLM_TeamTable(team_id=team_id, members_with_roles=[])
team_row = LiteLLM_TeamTable(team_id=team_id, team_alias="list-audit", members_with_roles=[])
created_user = LiteLLM_UserTable(
user_id=created_user_id, user_email="invitee@example.com", max_budget=None, spend=0.0, models=[]
)
@ -13319,6 +13322,7 @@ async def test_team_member_add_audits_a_user_created_from_a_list_payload(monkeyp
mock_audit.assert_called_once()
assert created_user_id not in mock_audit.call_args.kwargs["existing_user_ids"]
assert mock_audit.call_args.kwargs["team_alias"] == "list-audit"
class _RecordingAuditLogger(CustomLogger):
@ -13345,7 +13349,9 @@ async def _settle_audit_log_tasks() -> None:
def _team_roster_events(audit_logger: _RecordingAuditLogger, action: str) -> list[StandardAuditLogPayload]:
return [
p for p in audit_logger.payloads if p["table_name"] == LitellmTableNames.TEAM_TABLE_NAME and p["action"] == action
p
for p in audit_logger.payloads
if p["table_name"] == LitellmTableNames.TEAM_TABLE_NAME and p["action"] == action
]
@ -13354,6 +13360,11 @@ def _roster_user_roles(members_json: str | None) -> dict[str, str]:
return {m["user_id"]: m["role"] for m in json.loads(members_json)["members_with_roles"]}
def _roster_team_alias(members_json: str | None) -> str | None:
assert members_json is not None
return json.loads(members_json)["team_alias"]
@pytest.mark.asyncio
async def test_new_team_created_audit_event_carries_the_final_roster(monkeypatch):
from fastapi import Request
@ -13426,6 +13437,7 @@ async def test_team_member_delete_emits_a_roster_audit_event(monkeypatch, mock_d
team_row = MagicMock()
team_row.model_dump.return_value = {
"team_id": "team-del-audit",
"team_alias": "del-audit",
"members_with_roles": [
{"user_id": "alice", "user_email": None, "role": "admin"},
{"user_id": "bob", "user_email": None, "role": "user"},
@ -13459,6 +13471,8 @@ async def test_team_member_delete_emits_a_roster_audit_event(monkeypatch, mock_d
assert [e["object_id"] for e in updated_events] == ["team-del-audit"]
assert _roster_user_roles(updated_events[0]["before_value"]) == {"alice": "admin", "bob": "user"}
assert _roster_user_roles(updated_events[0]["updated_values"]) == {"alice": "admin"}
assert _roster_team_alias(updated_events[0]["before_value"]) == "del-audit"
assert _roster_team_alias(updated_events[0]["updated_values"]) == "del-audit"
stale_user_row = MagicMock()
stale_user_row.user_id = "carol"
@ -13483,6 +13497,7 @@ async def test_team_member_update_role_change_emits_a_roster_audit_event(monkeyp
mock_prisma_client = MagicMock()
team_row = LiteLLM_TeamTable(
team_id="team-role-audit",
team_alias="role-audit",
metadata={},
members_with_roles=[Member(user_id="alice", role="admin"), Member(user_id="bob", role="user")],
)
@ -13491,6 +13506,7 @@ async def test_team_member_update_role_change_emits_a_roster_audit_event(monkeyp
return {
"team_info": TeamInfoResponseObjectTeamTable(
team_id="team-role-audit",
team_alias="role-audit",
metadata={},
members_with_roles=(
TeamInfoMember(user_id="alice", role="admin", user_alias="Alice"),
@ -13531,6 +13547,7 @@ async def test_team_member_update_role_change_emits_a_roster_audit_event(monkeyp
assert [e["object_id"] for e in updated_events] == ["team-role-audit"]
assert _roster_user_roles(updated_events[0]["before_value"]) == {"alice": "admin", "bob": "user"}
assert _roster_user_roles(updated_events[0]["updated_values"]) == {"alice": "admin", "bob": "admin"}
assert _roster_team_alias(updated_events[0]["updated_values"]) == "role-audit"
await team_member_update(
data=TeamMemberUpdateRequest(team_id="team-role-audit", user_id="bob", role="admin"),