From 209eba6718b09689e25c3caf3cc358a926a67ff0 Mon Sep 17 00:00:00 2001 From: yucheng Date: Fri, 18 Sep 2026 23:28:39 +0000 Subject: [PATCH] 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> --- .../management_endpoints/team_endpoints.py | 15 ++++++++---- .../test_team_endpoints.py | 23 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 8e5d62976fa..81fd7f44538 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -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, diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index 67b8f3bc5f3..0077be9bfbb 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -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"),