diff --git a/litellm/__init__.py b/litellm/__init__.py index f67a252eb..9fb614396 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -60,6 +60,7 @@ _async_failure_callback: List[Callable] = ( pre_call_rules: List[Callable] = [] post_call_rules: List[Callable] = [] turn_off_message_logging: Optional[bool] = False +store_audit_logs = False # Enterprise feature, allow users to see audit logs ## end of callbacks ############# email: Optional[str] = ( diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 5b3f1db12..d80c7e9f6 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1278,6 +1278,21 @@ class LiteLLM_ErrorLogs(LiteLLMBase): endTime: Union[str, datetime, None] +class LiteLLM_AuditLogs(LiteLLMBase): + id: str + updated_at: datetime + changed_by: str + action: Literal["created", "updated", "deleted"] + table_name: Literal[ + LitellmTableNames.TEAM_TABLE_NAME, + LitellmTableNames.USER_TABLE_NAME, + LitellmTableNames.PROXY_MODEL_TABLE_NAME, + ] + object_id: str + before_value: Optional[Json] = None + updated_values: Optional[Json] = None + + class LiteLLM_SpendLogs_ResponseObject(LiteLLMBase): response: Optional[List[Union[LiteLLM_SpendLogs, Any]]] = None diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index e3d4effe8..88fc0e913 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -23,4 +23,5 @@ general_settings: master_key: sk-1234 litellm_settings: - callbacks: ["otel"] \ No newline at end of file + callbacks: ["otel"] + store_audit_logs: true \ No newline at end of file diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 689981960..afa188972 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10357,45 +10357,62 @@ async def new_team( } }, ) + + # Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True + if litellm.store_audit_logs is True: + _updated_values = complete_team_data.json(exclude_none=True) + _updated_values = json.dumps(_updated_values) + + asyncio.create_task( + create_audit_log_for_update( + request_data=LiteLLM_AuditLogs( + id=str(uuid.uuid4()), + updated_at=datetime.now(timezone.utc), + changed_by=user_api_key_dict.user_id or litellm_proxy_admin_name, + table_name=LitellmTableNames.TEAM_TABLE_NAME, + object_id=data.team_id, + action="created", + updated_values=_updated_values, + before_value=None, + ) + ) + ) + try: return team_row.model_dump() except Exception as e: return team_row.dict() -async def create_audit_log_for_update( - action: Literal["create", "update", "delete"], - # fyi: pylint does not directly allow you to pass Literal["LiteLLM_TeamTable"] - # because LiteLLM_TeamTable is also defined in _types.py - table_name: Literal[ - LitellmTableNames.TEAM_TABLE_NAME, - LitellmTableNames.USER_TABLE_NAME, - LitellmTableNames.PROXY_MODEL_TABLE_NAME, - ], - object_id: str, - changed_by: str, - before_value: dict, - after_value: dict, -): - if not premium_user: +async def create_audit_log_for_update(request_data: LiteLLM_AuditLogs): + if premium_user is not True: return - try: - pass + if litellm.store_audit_logs is not True: + return + if prisma_client is None: + raise Exception("prisma_client is None, no DB connected") + verbose_proxy_logger.debug("creating audit log for %s", request_data) + + if isinstance(request_data.updated_values, dict): + request_data.updated_values = json.dumps(request_data.updated_values) + + if isinstance(request_data.before_value, dict): + request_data.before_value = json.dumps(request_data.before_value) + + _request_data = request_data.dict(exclude_none=True) + + try: + await prisma_client.db.litellm_auditlog.create( + data={ + **_request_data, # type: ignore + } + ) except Exception as e: # [Non-Blocking Exception. Do not allow blocking LLM API call] verbose_proxy_logger.error(f"Failed Creating audit log {e}") - # await prisma_client.create_audit_log( - # data={ - # "action": action, - # "object_id": object_id, - # "changed_by": changed_by, - # "before_value": json.dumps(before_value), - # "after_value": json.dumps(after_value), - # } - # ) return @@ -10471,7 +10488,28 @@ async def update_team( team_id=data.team_id, ) - return team_row + # Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True + if litellm.store_audit_logs is True: + _before_value = existing_team_row.json(exclude_none=True) + _before_value = json.dumps(_before_value) + _after_value: str = json.dumps(updated_kv) + + asyncio.create_task( + create_audit_log_for_update( + request_data=LiteLLM_AuditLogs( + id=str(uuid.uuid4()), + updated_at=datetime.now(timezone.utc), + changed_by=user_api_key_dict.user_id or litellm_proxy_admin_name, + table_name=LitellmTableNames.TEAM_TABLE_NAME, + object_id=data.team_id, + action="updated", + updated_values=_after_value, + before_value=_before_value, + ) + ) + ) + + return team_row @router.post( @@ -10742,6 +10780,35 @@ async def delete_team( detail={"error": f"Team not found, passed team_id={team_id}"}, ) + # Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True + # we do this after the first for loop, since first for loop is for validation. we only want this inserted after validation passes + if litellm.store_audit_logs is True: + # make an audit log for each team deleted + for team_id in data.team_ids: + team_row = await prisma_client.get_data( # type: ignore + team_id=team_id, table_name="team", query_type="find_unique" + ) + + _team_row = team_row.json(exclude_none=True) + + asyncio.create_task( + create_audit_log_for_update( + request_data=LiteLLM_AuditLogs( + id=str(uuid.uuid4()), + updated_at=datetime.now(timezone.utc), + changed_by=user_api_key_dict.user_id + or litellm_proxy_admin_name, + table_name=LitellmTableNames.TEAM_TABLE_NAME, + object_id=team_id, + action="deleted", + updated_values="{}", + before_value=_team_row, + ) + ) + ) + + # End of Audit logging + ## DELETE ASSOCIATED KEYS await prisma_client.delete_data(team_id_list=data.team_ids, table_name="key") ## DELETE TEAMS diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 243f06337..7cc688ee8 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -243,4 +243,16 @@ model LiteLLM_InvitationLink { liteLLM_user_table_user LiteLLM_UserTable @relation("UserId", fields: [user_id], references: [user_id]) liteLLM_user_table_created LiteLLM_UserTable @relation("CreatedBy", fields: [created_by], references: [user_id]) liteLLM_user_table_updated LiteLLM_UserTable @relation("UpdatedBy", fields: [updated_by], references: [user_id]) +} + + +model LiteLLM_AuditLog { + id String @id @default(uuid()) + updated_at DateTime @default(now()) + changed_by String // user or system that performed the action + action String // create, update, delete + table_name String // on of LitellmTableNames.TEAM_TABLE_NAME, LitellmTableNames.USER_TABLE_NAME, LitellmTableNames.PROXY_MODEL_TABLE_NAME, + object_id String // id of the object being audited. This can be the key id, team id, user id, model id + before_value Json? // value of the row + updated_values Json? // value of the row after change } \ No newline at end of file diff --git a/schema.prisma b/schema.prisma index b207363fb..7cc688ee8 100644 --- a/schema.prisma +++ b/schema.prisma @@ -246,13 +246,13 @@ model LiteLLM_InvitationLink { } -model AuditLog { - id Int @id @default(autoincrement()) +model LiteLLM_AuditLog { + id String @id @default(uuid()) updated_at DateTime @default(now()) changed_by String // user or system that performed the action action String // create, update, delete - object_type String // team, key, user, model + table_name String // on of LitellmTableNames.TEAM_TABLE_NAME, LitellmTableNames.USER_TABLE_NAME, LitellmTableNames.PROXY_MODEL_TABLE_NAME, object_id String // id of the object being audited. This can be the key id, team id, user id, model id before_value Json? // value of the row - after_value Json? // value of the row after change + updated_values Json? // value of the row after change } \ No newline at end of file