add file
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user