"""平台访问审计中间件(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