refactor(audit): 审计架构清理——复活白名单监听器、停用噪声监听器、清除僵尸装饰器
一、统一为单一监听器实现
原先两套 SQLAlchemy 事件监听器并存:
· app/utils/audit_events.py —— 全局监听 db.Model、无白名单、无请求上下文守卫(实际在跑)
· app/core/audit_listener.py —— 白名单制、有守卫、有模型级开关(从未生效)
后者失效的根因:注册代码写在 extensions.py 的 init_extensions() 内,
而该函数全仓库只有定义、没有任何调用(create_app 直接内联调用 db.init_app 等)。
现统一由 app/core/audit_listener.py 承担,并在 create_app() 中显式注册。
extensions.py 的死函数 init_extensions 整体删除,避免后人误以为它是有效入口。
二、修复监听器三处致命缺陷(此前注册了也写不进数据)
1. 事件回调第二个参数是 Connection,原代码却调用 Connection.add()(不存在),
每次写日志都抛 AttributeError 并被 except 吞掉 → 改为 connection.execute()
2. register_audit_listeners 从 app.models 批量 import 多个未导出的模型,
ImportError 被上层 try/except 吞掉 → 改为按表名从 db.metadata 取模型
3. 本项目有 31 处函数体内延迟导入模型(如 scrap.py 内部才 import ScrapApproval),
一次性注册会静默漏表 → 增加 ensure_audit_listeners() 惰性补绑,
并在模型预加载段补全审批单/BOM/采购等模型
三、强约束
· WHITELIST_TABLES:仅 18 张核心业务表,系统表/草稿表/向量表不再自审
· has_request_context() 守卫:系统初始化与后台定时任务不再产生 username=system 噪声
· IGNORE_FIELDS 增加 password/password_hash/salt/token/secret/api_key(安全红线)
· created_at 显式写 beijing_time(),与全系统时间口径一致
四、清除僵尸装饰器
@audit_log 早已退化为直接透传的空壳(module/action 参数全被忽略,
数据库中零星的中文 action 即其历史遗留产物),却仍挂在 38 处路由上。
连同 13 个文件的 import 一并移除;audit_events.register_audit_events 改为空操作。
验证:应用上下文中的写操作不产生日志;HTTP 请求产生 5 条日志,
对象为业务单号(APR-SCRAP-... / SKU),模块中文,操作人真实,时间为北京时间。
This commit is contained in:
@ -1,8 +1,20 @@
|
||||
# inventory-backend/app/utils/audit_events.py
|
||||
"""
|
||||
全局无侵入的审计日志拦截器
|
||||
监听所有模型的增删改操作,自动提取旧值和新值存入 audit_logs 表
|
||||
完美对接前端 AuditLog.vue 的解析逻辑 (changes, deleted_snapshot, created)
|
||||
[已停用 — DEPRECATED] 全局审计日志拦截器
|
||||
|
||||
★ 本模块自 v3.x 起不再注册、不再生效。
|
||||
审计日志已统一由 app/core/audit_listener.py 接管(白名单制 + 请求上下文守卫)。
|
||||
|
||||
停用原因(原实现的三个设计缺陷):
|
||||
1. 监听 db.Model 全局,无白名单 —— 系统表/草稿表/向量表被一并审计,
|
||||
产生大量无业务价值的记录;
|
||||
2. 无 has_request_context() 守卫 —— 系统初始化与后台定时任务产生的变更
|
||||
被记为 username='system' 的日志(历史数据中占 32%);
|
||||
3. target_name 兜底为 `{英文表名} ID:{id}`(历史数据中占 72%),
|
||||
对用户完全不可读。
|
||||
|
||||
保留文件仅为兼容可能的历史 import;register_audit_events() 已改为空操作,
|
||||
调用它不会产生任何监听器。
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime, date
|
||||
@ -208,116 +220,11 @@ def _has_changes(history):
|
||||
"""检查历史记录对象是否有变更"""
|
||||
return history.has_changes()
|
||||
|
||||
|
||||
def register_audit_events(db):
|
||||
"""
|
||||
全局注册审计事件监听器
|
||||
监听所有模型的 INSERT/UPDATE/DELETE 事件
|
||||
[已停用] 空操作,保留仅为兼容历史调用。
|
||||
|
||||
审计日志已由 app/core/audit_listener.py 接管(白名单制 + 请求上下文守卫)。
|
||||
调用本函数不会注册任何监听器。
|
||||
"""
|
||||
from sqlalchemy import inspect
|
||||
|
||||
@event.listens_for(db.Model, 'before_update', propagate=True)
|
||||
def before_update_listener(mapper, connection, target):
|
||||
"""UPDATE 事件:抓取字段变更明细"""
|
||||
if target.__tablename__ in IGNORE_TABLES:
|
||||
return
|
||||
|
||||
try:
|
||||
state = inspect(target)
|
||||
changes = {}
|
||||
|
||||
for attr in state.attrs:
|
||||
prop = attr.key
|
||||
|
||||
# 跳过忽略字段
|
||||
if prop in IGNORE_FIELDS:
|
||||
continue
|
||||
|
||||
# 跳过关系属性
|
||||
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
|
||||
continue
|
||||
|
||||
if _has_changes(attr.history):
|
||||
old_value = attr.history.deleted[0] if attr.history.deleted else None
|
||||
new_value = attr.history.added[0] if attr.history.added else None
|
||||
|
||||
# 序列化值
|
||||
old_serialized = serialize_value(old_value)
|
||||
new_serialized = serialize_value(new_value)
|
||||
|
||||
# 只记录真正变化的字段
|
||||
if old_serialized != new_serialized:
|
||||
changes[prop] = {
|
||||
'old': old_serialized,
|
||||
'new': new_serialized
|
||||
}
|
||||
|
||||
if changes:
|
||||
insert_audit_log(connection, 'UPDATE', target, {'changes': changes})
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.error(f"Audit Update Error: {e}")
|
||||
|
||||
@event.listens_for(db.Model, 'before_delete', propagate=True)
|
||||
def before_delete_listener(mapper, connection, target):
|
||||
"""DELETE 事件:抓取被删除对象的完整快照"""
|
||||
if target.__tablename__ in IGNORE_TABLES:
|
||||
return
|
||||
|
||||
try:
|
||||
state = inspect(target)
|
||||
snapshot = {}
|
||||
|
||||
for attr in state.attrs:
|
||||
prop = attr.key
|
||||
|
||||
# 跳过忽略字段
|
||||
if prop in IGNORE_FIELDS:
|
||||
continue
|
||||
|
||||
# 跳过关系属性
|
||||
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
|
||||
continue
|
||||
|
||||
value = getattr(target, prop, None)
|
||||
snapshot[prop] = serialize_value(value)
|
||||
|
||||
insert_audit_log(connection, 'DELETE', target, {'deleted_snapshot': snapshot})
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.error(f"Audit Delete Error: {e}")
|
||||
|
||||
@event.listens_for(db.Model, 'after_insert', propagate=True)
|
||||
def after_insert_listener(mapper, connection, target):
|
||||
"""INSERT 事件:抓取新增对象的完整快照"""
|
||||
if target.__tablename__ in IGNORE_TABLES:
|
||||
return
|
||||
|
||||
try:
|
||||
state = inspect(target)
|
||||
snapshot = {}
|
||||
|
||||
for attr in state.attrs:
|
||||
prop = attr.key
|
||||
|
||||
# 跳过忽略字段
|
||||
if prop in IGNORE_FIELDS:
|
||||
continue
|
||||
|
||||
# 跳过关系属性
|
||||
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
|
||||
continue
|
||||
|
||||
value = getattr(target, prop, None)
|
||||
snapshot[prop] = serialize_value(value)
|
||||
|
||||
insert_audit_log(connection, 'CREATE', target, {'created': snapshot})
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.error(f"Audit Insert Error: {e}")
|
||||
|
||||
# 返回注册成功信息
|
||||
return True
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user