一、流程文档(按 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)
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
"""平台访问审计中间件(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
|