mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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:
parent
2a7dcc77b2
commit
209eba6718
2 changed files with 31 additions and 7 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue