81 lines
3.1 KiB
Python
81 lines
3.1 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)", exc_info=True)
|
|||
|
|
return response
|