This commit is contained in:
Your Name
2026-07-20 14:46:47 +08:00
parent bca0da7c7d
commit 845fefd6cf
6 changed files with 159 additions and 7 deletions
+127 -2
View File
@@ -1,4 +1,4 @@
from typing import Optional, List
from typing import Optional, List, Any
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
import json
@@ -7,9 +7,134 @@ from app.models.audit import AuditLog, AuditAction, AuditResource
from app.models.auth import User
# ===== 兼容层:老 API(字符串 action/resource)映射到新枚举 =====
# 字符串 action -> 枚举 + 资源类型;用于兼容各 endpoint 里硬编码的 "network.create" 等
_ACTION_MAP = {
# network
"network.create": (AuditAction.CREATE, AuditResource.NETWORK),
"network.update": (AuditAction.UPDATE, AuditResource.NETWORK),
"network.delete": (AuditAction.DELETE, AuditResource.NETWORK),
# ip
"ip.update": (AuditAction.UPDATE, AuditResource.IP_ADDRESS),
"ip.reserve": (AuditAction.UPDATE, AuditResource.IP_ADDRESS),
"ip.unreserve": (AuditAction.UPDATE, AuditResource.IP_ADDRESS),
# scan
"scan.trigger": (AuditAction.SCAN, AuditResource.SCAN_TASK),
# snmp
"snmp.credential.create": (AuditAction.CREATE, AuditResource.SNMP_CREDENTIAL),
"snmp.credential.update": (AuditAction.UPDATE, AuditResource.SNMP_CREDENTIAL),
"snmp.credential.delete": (AuditAction.DELETE, AuditResource.SNMP_CREDENTIAL),
"snmp.device.create": (AuditAction.CREATE, AuditResource.SNMP_DEVICE),
"snmp.device.update": (AuditAction.UPDATE, AuditResource.SNMP_DEVICE),
"snmp.device.delete": (AuditAction.DELETE, AuditResource.SNMP_DEVICE),
# alert
"alert.acknowledge": (AuditAction.UPDATE, AuditResource.ALERT),
"alert.resolve": (AuditAction.UPDATE, AuditResource.ALERT),
# user
"user.login": (AuditAction.LOGIN, AuditResource.USER),
"user.login.failed": (AuditAction.LOGIN, AuditResource.USER),
"user.logout": (AuditAction.LOGOUT, AuditResource.USER),
"user.create": (AuditAction.CREATE, AuditResource.USER),
"user.update_status": (AuditAction.UPDATE, AuditResource.USER),
"user.unlock": (AuditAction.UPDATE, AuditResource.USER),
"user.reset_password": (AuditAction.UPDATE, AuditResource.USER),
"user.delete": (AuditAction.DELETE, AuditResource.USER),
"user.change_password": (AuditAction.UPDATE, AuditResource.USER),
}
_RESOURCE_MAP = {
"network": AuditResource.NETWORK,
"ip": AuditResource.IP_ADDRESS,
"snmp_credential": AuditResource.SNMP_CREDENTIAL,
"snmp_device": AuditResource.SNMP_DEVICE,
"user": AuditResource.USER,
"alert": AuditResource.ALERT,
"scan": AuditResource.SCAN_TASK,
}
class AuditService:
"""审计日志服务"""
@staticmethod
def record(
db: Session,
*,
action: str,
user=None,
resource_type: Optional[str] = None,
resource_id: Optional[Any] = None,
resource_name: Optional[str] = None,
method: Optional[str] = None,
path: Optional[str] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None,
status: str = "success",
detail: Optional[Any] = None,
) -> Optional["AuditLog"]:
"""
兼容层:将老版 API(字符串 action/resource)映射到新版 log()。
所有 endpoint 都通过本方法写审计,避免直接调用新签名造成 500。
任意环节失败都安全降级(不抛异常),审计写入失败不影响主业务。
"""
try:
# action 字符串 -> (枚举, 资源枚举) ;未知 action 用 OTHER + SYSTEM
enum_action, default_resource = _ACTION_MAP.get(
action, (AuditAction.OTHER, AuditResource.SYSTEM)
)
resource_enum = _RESOURCE_MAP.get(resource_type, default_resource)
# 详情拆分:dict 拆出 old/new;其余当 description
old_value = None
new_value = None
description = None
error_message = None
success = status != "failed"
if isinstance(detail, dict):
old_value = detail.get("before")
new_value = detail.get("after")
# 把 action 原始字符串 + 资源名放到 description 里,保留细节
desc_parts = [action]
if resource_name:
desc_parts.append(resource_name)
if status == "failed":
error_message = detail.get("error") or detail.get("message") or json.dumps(detail, ensure_ascii=False, default=str)
description = " | ".join(desc_parts)
else:
description = f"{action} | {resource_name or ''}"
if status == "failed" and detail:
error_message = str(detail)
# user 不能 None 传给 log()fallback 到 User(id=None, username='anonymous')
# 但 log() 会用 user.id / user.username / user.real_name,如果都是 None 就 OK
if user is None:
anon = User(id=None, username="anonymous", real_name=None)
else:
anon = user
return AuditService.log(
db=db,
user=anon,
action=enum_action,
resource=resource_enum,
resource_id=str(resource_id) if resource_id is not None else None,
description=description[:500] if description else None,
old_value=old_value,
new_value=new_value,
user_ip=ip_address,
user_agent=(user_agent or "")[:500],
request_method=method,
request_path=path,
success=success,
error_message=error_message,
)
except Exception as e:
db.rollback()
print(f"[audit] record() failed action={action}: {e}")
return None
@staticmethod
def log(db: Session,
user: User,