Files
group_xinghuo_jinrong/app/api/audit_middleware.py
T
GaoYiYuan_0626 037ce7edca docs(架构改进): 补齐 PRD/开发计划/TODO/交接文档,落地无密钥告警与 Redis 分布式锁
一、流程文档(按 AIcoding 六步落地,供新会话从交接文档开工)
- 新增 docs/PRD/PRD-架构改进与稳定性加固.md:6 条 FR(文档勘误、非缺陷说明、
  无密钥启动告警、审计失败告警、Redis 分布式锁、中间件顺序测试)
- 新增 docs/项目框架设计/改进方案评审-问题清单与对比.md:24 项问题分档 A~G,
  经两轮独立 AI 评审,无阻断级错误
- 新增 docs/项目框架设计/开发计划-架构改进.md:HOW 层设计,含合并前只做低风险
  11 项的批次策略
- 新增 docs/项目框架设计/TODO-架构改进.md:T-101~T-109、T-201~T-202 可勾选项
- 新增 docs/交接文档-架构改进.md:自包含交接入口,hy3 新会话可直接开工
- 新增 docs/项目框架设计/架构设计说明书.md:按模块/分层逐一讲解的全量架构说明

二、代码改动(T-107/108/109、T-201.1、T-201.2)
- app/main.py:启动时 DEEPSEEK_API_KEY 缺失告警,明确告知将走降级回复
- app/utils/authz.py:越权审计失败日志补 trace_id,便于串联全链路
- app/api/audit_middleware.py:审计失败日志补 status/path/request_id
- app/service/risk/redis_gateway.py:新增 acquire_lock(SET NX EX)与
  release_lock(Lua 原子释放,只删自己的锁)
- app/service/risk/locks.py:run_locked 改为双层锁,Redis 为主、进程内锁为备;
  Redis 超时沿用 fn(locked=False) 降级语义,Redis 不可用(含测试 Fake 缺方法的
  AttributeError)安全退回进程内锁,绝不抛异常

三、文档勘误(A1/A2/A3)
- MEMORY.md:文件数 42→45、Tools 4→5
- 02-mysql-agent专用.sql:会话表 5→6
- 架构设计-风控模块.md:同步更正

四、测试
- 新增 tests/test_locks_redis.py:覆盖抢锁成功、占用超时、Redis 故障降级、
  Fake 缺方法降级、只删自己锁、三处调用点 key 前缀
- tests/test_audit_middleware.py:补充告警字段断言
- 全量 pytest 510 passed(原基线 503)
2026-09-09 18:10:03 +08:00

87 lines
3.2 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.
"""平台访问审计中间件(T-02 · F-02 全量留痕 / 手册 P-05)。
每个非排障路径请求落一行 audit_log(event_type='http_access',agent_type=
'platform'——与 simulate 网关审计口径一致):method/path/status/latency_ms/
request_id 进 input_summary,actor 取 deps 鉴权成功后注入的 request.state.auth
(401 时为 anonymous)。写库失败降级 warning 不阻塞响应(业务层关键判定审计
[authz/trade_event/suitability_check 等] 不经此路径,不受降级影响)。
注册顺序:trace 中间件之后注册(即执行序在 trace 之内),保证审计时
trace_id/request_id 已绑定。
"""
from __future__ import annotations
import logging
import time
from fastapi import Request
from starlette.responses import Response
from app.repository.risk_repository import RiskRepository
from app.utils.trace import current_request_id, current_trace
logger = logging.getLogger(__name__)
# 排障/文档路径不落访问审计(健康探活噪音;docs 页面无业务语义)
_SKIP_PATHS = {"/health", "/openapi.json", "/docs", "/redoc", "/favicon.ico"}
def _repo() -> RiskRepository:
"""审计仓储入口(测试 monkeypatch 点,与 api 路由模式一致)。"""
return RiskRepository()
def write_http_access(request: Request, status: int, latency_ms: int) -> None:
"""单请求访问审计(INSERT-only;audit_middleware 调用,测试可直调)。"""
auth = getattr(request.state, "auth", None)
_repo().insert_audit_log(
{
"trace_id": current_trace(),
"event_type": "http_access",
"agent_type": "platform",
"actor_id": auth.actor_id if auth is not None else "anonymous",
"customer_id": None,
"rule_id": None,
"input_summary": {
"method": request.method,
"path": request.url.path,
"status": status,
"latency_ms": latency_ms,
"request_id": current_request_id(),
},
"decision": str(status),
"risk_score": None,
"handler_id": None,
"handler_result": None,
"handler_comment": None,
}
)
async def audit_middleware(request: Request, call_next) -> Response:
"""http_access 中间件:异常请求留痕 500 后继续抛(trace 层兜底出错误体)。"""
if request.url.path in _SKIP_PATHS or request.method == "OPTIONS":
return await call_next(request)
started = time.perf_counter()
try:
response = await call_next(request)
status = response.status_code
except Exception:
try:
write_http_access(request, 500, int((time.perf_counter() - started) * 1000))
except Exception:
logger.warning("http_access audit failed on 500 path", exc_info=True)
raise
try:
write_http_access(request, status, int((time.perf_counter() - started) * 1000))
except Exception:
logger.warning(
"http_access audit failed (degrade, not blocking): status=%s path=%s request_id=%s",
status,
request.url.path,
current_request_id(),
exc_info=True,
)
return response