Files
Aether/_deprecated_py_src/services/system/audit.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

463 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
审计日志服务
记录所有重要操作和安全事件
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.database import get_db
from src.models.database import AuditEventType, AuditLog
# 审计模型已移至 src/models/database.py
class AuditService:
"""审计服务
事务策略:本服务不负责事务提交,由中间件统一管理。
所有方法只做 db.add/flush提交由请求结束时的中间件处理。
"""
@staticmethod
def log_event(
db: Session,
event_type: AuditEventType,
description: str,
user_id: str | None = None, # UUID
api_key_id: str | None = None, # UUID
ip_address: str | None = None,
user_agent: str | None = None,
request_id: str | None = None,
status_code: int | None = None,
error_message: str | None = None,
metadata: dict[str, Any] | None = None,
) -> AuditLog:
"""
记录审计事件
Args:
db: 数据库会话
event_type: 事件类型
description: 事件描述
user_id: 用户ID
api_key_id: API密钥ID
ip_address: IP地址
user_agent: 用户代理
request_id: 请求ID
status_code: 状态码
error_message: 错误消息
metadata: 额外元数据
Returns:
审计日志记录
Note:
不在此方法内提交事务,由调用方或中间件统一管理。
"""
audit_log = AuditLog(
event_type=event_type.value,
description=description,
user_id=user_id,
api_key_id=api_key_id,
ip_address=ip_address,
user_agent=user_agent,
request_id=request_id,
status_code=status_code,
error_message=error_message,
event_metadata=metadata,
)
db.add(audit_log)
# 使用 flush 使记录可见但不提交事务,事务由中间件统一管理
db.flush()
# 同时记录到系统日志
# 检查 metadata 中是否有 quiet_logging 标志(由高频轮询端点设置)
quiet_logging = metadata.get("quiet_logging", False) if metadata else False
if not quiet_logging:
log_message = (
f"AUDIT [{event_type.value}] - {description} | "
f"user_id={user_id}, ip={ip_address}"
)
if event_type in [
AuditEventType.UNAUTHORIZED_ACCESS,
AuditEventType.SUSPICIOUS_ACTIVITY,
]:
logger.warning(log_message)
elif event_type in [AuditEventType.LOGIN_FAILED, AuditEventType.REQUEST_FAILED]:
logger.info(log_message)
# request_success 已由 Pipeline 日志覆盖,不再重复输出到控制台
return audit_log
@staticmethod
def log_login_attempt(
db: Session,
email: str,
success: bool,
ip_address: str,
user_agent: str,
user_id: str | None = None, # UUID
error_reason: str | None = None,
) -> Any:
"""
记录登录尝试
Args:
db: 数据库会话
email: 登录邮箱
success: 是否成功
ip_address: IP地址
user_agent: 用户代理
user_id: 用户ID成功时
error_reason: 失败原因
"""
event_type = AuditEventType.LOGIN_SUCCESS if success else AuditEventType.LOGIN_FAILED
description = f"Login attempt for {email}"
if not success and error_reason:
description += f": {error_reason}"
AuditService.log_event(
db=db,
event_type=event_type,
description=description,
user_id=user_id,
ip_address=ip_address,
user_agent=user_agent,
metadata={"email": email},
)
@staticmethod
def log_api_request(
db: Session,
user_id: str, # UUID
api_key_id: str, # UUID
request_id: str,
model: str,
provider: str,
success: bool,
ip_address: str,
status_code: int,
error_message: str | None = None,
input_tokens: int | None = None,
output_tokens: int | None = None,
cost_usd: float | None = None,
) -> Any:
"""
记录API请求
Args:
db: 数据库会话
user_id: 用户ID
api_key_id: API密钥ID
request_id: 请求ID
model: 模型名称
provider: 提供商名称
success: 是否成功
ip_address: IP地址
status_code: 状态码
error_message: 错误消息
input_tokens: 输入tokens
output_tokens: 输出tokens
cost_usd: 成本(美元)
"""
event_type = AuditEventType.REQUEST_SUCCESS if success else AuditEventType.REQUEST_FAILED
description = f"API request to {provider}/{model}"
metadata = {"model": model, "provider": provider}
if input_tokens:
metadata["input_tokens"] = input_tokens
if output_tokens:
metadata["output_tokens"] = output_tokens
if cost_usd:
metadata["cost_usd"] = cost_usd
AuditService.log_event(
db=db,
event_type=event_type,
description=description,
user_id=user_id,
api_key_id=api_key_id,
request_id=request_id,
ip_address=ip_address,
status_code=status_code,
error_message=error_message,
metadata=metadata,
)
@staticmethod
def log_security_event(
db: Session,
event_type: AuditEventType,
description: str,
ip_address: str,
user_id: str | None = None, # UUID
severity: str = "medium",
details: dict[str, Any] | None = None,
) -> Any:
"""
记录安全事件
Args:
db: 数据库会话
event_type: 事件类型
description: 事件描述
ip_address: IP地址
user_id: 用户ID
severity: 严重程度 (low, medium, high, critical)
details: 详细信息
"""
event_metadata = {"severity": severity}
if details:
event_metadata.update(details)
AuditService.log_event(
db=db,
event_type=event_type,
description=description,
user_id=user_id,
ip_address=ip_address,
metadata=event_metadata,
)
# 对于高严重性事件,简化日志输出
if severity in ["high", "critical"]:
logger.error(f"安全告警 [{severity.upper()}]: {description}")
@staticmethod
def get_user_audit_logs(
db: Session,
user_id: str, # UUID
event_types: list[AuditEventType] | None = None,
limit: int = 100,
) -> list[AuditLog]:
"""
获取用户的审计日志
Args:
db: 数据库会话
user_id: 用户ID
event_types: 事件类型过滤
limit: 返回数量限制
Returns:
审计日志列表
"""
query = db.query(AuditLog).filter(AuditLog.user_id == user_id)
if event_types:
event_type_values = [et.value for et in event_types]
query = query.filter(AuditLog.event_type.in_(event_type_values))
return query.order_by(AuditLog.created_at.desc()).limit(limit).all()
@staticmethod
def get_suspicious_activities(db: Session, hours: int = 24, limit: int = 100) -> list[AuditLog]:
"""
获取可疑活动
Args:
db: 数据库会话
hours: 时间范围(小时)
limit: 返回数量限制
Returns:
可疑活动列表
"""
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=hours)
suspicious_types = [
AuditEventType.SUSPICIOUS_ACTIVITY.value,
AuditEventType.UNAUTHORIZED_ACCESS.value,
AuditEventType.LOGIN_FAILED.value,
AuditEventType.REQUEST_RATE_LIMITED.value,
]
return (
db.query(AuditLog)
.filter(AuditLog.event_type.in_(suspicious_types), AuditLog.created_at >= cutoff_time)
.order_by(AuditLog.created_at.desc())
.limit(limit)
.all()
)
@staticmethod
def analyze_user_behavior(db: Session, user_id: str, days: int = 30) -> dict[str, Any]: # UUID
"""
分析用户行为
Args:
db: 数据库会话
user_id: 用户ID
days: 分析天数
Returns:
行为分析结果
"""
from sqlalchemy import func
cutoff_time = datetime.now(timezone.utc) - timedelta(days=days)
# 统计各种事件类型
event_counts = (
db.query(AuditLog.event_type, func.count(AuditLog.id).label("count"))
.filter(AuditLog.user_id == user_id, AuditLog.created_at >= cutoff_time)
.group_by(AuditLog.event_type)
.all()
)
# 统计失败请求
failed_requests = (
db.query(func.count(AuditLog.id))
.filter(
AuditLog.user_id == user_id,
AuditLog.event_type == AuditEventType.REQUEST_FAILED.value,
AuditLog.created_at >= cutoff_time,
)
.scalar()
)
# 统计成功请求
success_requests = (
db.query(func.count(AuditLog.id))
.filter(
AuditLog.user_id == user_id,
AuditLog.event_type == AuditEventType.REQUEST_SUCCESS.value,
AuditLog.created_at >= cutoff_time,
)
.scalar()
)
# 获取最近的可疑活动
recent_suspicious = int(
db.query(func.count(AuditLog.id))
.filter(
AuditLog.user_id == user_id,
AuditLog.event_type.in_(
[
AuditEventType.SUSPICIOUS_ACTIVITY.value,
AuditEventType.UNAUTHORIZED_ACCESS.value,
]
),
AuditLog.created_at >= cutoff_time,
)
.scalar()
or 0
)
return {
"user_id": user_id,
"period_days": days,
"event_counts": {event: count for event, count in event_counts},
"failed_requests": failed_requests or 0,
"success_requests": success_requests or 0,
"success_rate": (
success_requests / (success_requests + failed_requests)
if (success_requests + failed_requests) > 0
else 0
),
"suspicious_activities": recent_suspicious,
"analysis_time": datetime.now(timezone.utc).isoformat(),
}
@staticmethod
def log_event_auto(
event_type: AuditEventType,
description: str,
user_id: str | None = None,
api_key_id: str | None = None,
ip_address: str | None = None,
user_agent: str | None = None,
request_id: str | None = None,
status_code: int | None = None,
error_message: str | None = None,
event_metadata: dict[str, Any] | None = None,
db: Session | None = None,
) -> AuditLog | None:
"""
自动管理数据库会话的审计日志记录方法
适用于中间件等无法直接获取数据库会话的场景
Args:
event_type: 事件类型
description: 事件描述
user_id: 用户ID
api_key_id: API密钥ID
ip_address: IP地址
user_agent: 用户代理
request_id: 请求ID
status_code: 状态码
error_message: 错误消息
event_metadata: 额外元数据
db: 数据库会话(可选,如不提供则自动创建)
Returns:
审计日志记录
"""
# 如果提供了数据库会话,使用它(不自动提交)
if db is not None:
try:
audit_log = AuditService.log_event(
db=db,
event_type=event_type,
description=description,
user_id=user_id,
api_key_id=api_key_id,
ip_address=ip_address,
user_agent=user_agent,
request_id=request_id,
status_code=status_code,
error_message=error_message,
metadata=event_metadata,
)
# 注意:不在这里提交,让调用方决定何时提交
return audit_log
except Exception as e:
logger.error(f"Failed to log audit event: {e}")
return None
# 如果没有提供会话,自动创建并管理
db_session = None
try:
db_session = next(get_db())
audit_log = AuditService.log_event(
db=db_session,
event_type=event_type,
description=description,
user_id=user_id,
api_key_id=api_key_id,
ip_address=ip_address,
user_agent=user_agent,
request_id=request_id,
status_code=status_code,
error_message=error_message,
metadata=event_metadata,
)
db_session.commit()
return audit_log
except Exception as e:
logger.error(f"Failed to log audit event with auto session: {e}")
if db_session is not None:
db_session.rollback()
return None
finally:
if db_session is not None:
db_session.close()
# 全局审计服务实例
audit_service = AuditService()