mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-05 17:22:28 +08:00
- Remove unused context.py module (replaced by request.state) - Remove provider_cache.py (no longer needed) - Unify environment loading in config/settings.py instead of __init__.py - Add deprecation warning for get_async_db() (consolidating on sync Session) - Enhance database.py documentation with comprehensive transaction strategy - Simplify audit logging to reuse request-level Session (no separate connections) - Extract UsageService._build_usage_params() helper to reduce code duplication - Update model and user cache implementations with refined transaction handling - Remove unnecessary sessionmaker from pipeline - Clean up audit service exception handling
459 lines
14 KiB
Python
459 lines
14 KiB
Python
"""
|
||
审计日志服务
|
||
记录所有重要操作和安全事件
|
||
"""
|
||
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
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: Optional[str] = None, # UUID
|
||
api_key_id: Optional[str] = None, # UUID
|
||
ip_address: Optional[str] = None,
|
||
user_agent: Optional[str] = None,
|
||
request_id: Optional[str] = None,
|
||
status_code: Optional[int] = None,
|
||
error_message: Optional[str] = None,
|
||
metadata: Optional[Dict[str, Any]] = 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()
|
||
|
||
# 同时记录到系统日志
|
||
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)
|
||
else:
|
||
logger.debug(log_message)
|
||
|
||
return audit_log
|
||
|
||
@staticmethod
|
||
def log_login_attempt(
|
||
db: Session,
|
||
email: str,
|
||
success: bool,
|
||
ip_address: str,
|
||
user_agent: str,
|
||
user_id: Optional[str] = None, # UUID
|
||
error_reason: Optional[str] = None,
|
||
):
|
||
"""
|
||
记录登录尝试
|
||
|
||
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: Optional[str] = None,
|
||
input_tokens: Optional[int] = None,
|
||
output_tokens: Optional[int] = None,
|
||
cost_usd: Optional[float] = None,
|
||
):
|
||
"""
|
||
记录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: Optional[str] = None, # UUID
|
||
severity: str = "medium",
|
||
details: Optional[Dict[str, Any]] = None,
|
||
):
|
||
"""
|
||
记录安全事件
|
||
|
||
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: Optional[List[AuditEventType]] = 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 = (
|
||
db.query(AuditLog)
|
||
.filter(
|
||
AuditLog.user_id == user_id,
|
||
AuditLog.event_type.in_(
|
||
[
|
||
AuditEventType.SUSPICIOUS_ACTIVITY.value,
|
||
AuditEventType.UNAUTHORIZED_ACCESS.value,
|
||
]
|
||
),
|
||
AuditLog.created_at >= cutoff_time,
|
||
)
|
||
.count()
|
||
)
|
||
|
||
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: Optional[str] = None,
|
||
api_key_id: Optional[str] = None,
|
||
ip_address: Optional[str] = None,
|
||
user_agent: Optional[str] = None,
|
||
request_id: Optional[str] = None,
|
||
status_code: Optional[int] = None,
|
||
error_message: Optional[str] = None,
|
||
event_metadata: Optional[Dict[str, Any]] = None,
|
||
db: Optional[Session] = None,
|
||
) -> Optional[AuditLog]:
|
||
"""
|
||
自动管理数据库会话的审计日志记录方法
|
||
适用于中间件等无法直接获取数据库会话的场景
|
||
|
||
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()
|