fix: B6 评审修复——鉴权审计统一 deny/401 留痕、start_date/end_date 对齐 PRD、simulate 回挂鉴权、异常与审计注入收敛
- P1-1: 403/401 全部经 deps.deny/_authz_audit 留痕(event_type='authz') - P2-1: GET /alerts 参数名 start_date/end_date 对齐 PRD;page/page_size 用 fastapi.Query - P2-2: /api/simulate/trade 回挂 get_auth_context(risk_demo 或本人),越权 403+审计 - P2-3: suitability/aml 直调补审计+request_ref 透传 - P3-1: 多角色 fail-closed 口径固化进测试;P3-3/P3-4 core_ro/risk_repo 必传; P3-5 LookupError→NotFoundError 统一;P3-9 alert_service import 上提 - 测试: test_risk_api 权限矩阵/审计断言/多角色组合/非 dev 拒绝; test_trade_gateway 客户本人 vs 越权 403(P3-4 模拟 _repo 注入 sqlite) - 挂账: P3-2/P3-7(响应外壳+disclaimer)→B7;P3-6(scan 幂等)→B9b 前
This commit is contained in:
+18
-5
@@ -1,7 +1,8 @@
|
||||
"""模拟交易网关路由(PRD FR-1 · 薄路由,不含业务)。
|
||||
|
||||
鉴权:依赖 T-01 JWT(risk_demo 或客户本人 customer_id == subject);
|
||||
`get_auth_context` B6 落地后在此挂 `Depends`,本路由签名不变(开发计划 B5/B6 边界)。
|
||||
鉴权:`Depends(get_auth_context)`(B6 回挂,评审 P2-2)——一期接受
|
||||
risk_demo 演示账号或客户本人(auth.customer_id == 请求 customer_id,
|
||||
PRD FR-1 §鉴权);越权经 deps.deny 审计后 403。T-01 后工厂内部换 JWT。
|
||||
trace:B7 中间件贯通;B7 前由 service 层 ensure_trace 兜底。
|
||||
挂载:B7 集成 main.py(当前仅 TestClient 独立挂 router 验证)。
|
||||
统一响应外壳:utils/response.py 为占位(P1 任务),落地点挂账 B7(届时
|
||||
@@ -12,14 +13,21 @@ from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.deps import AuthContext, deny, get_auth_context
|
||||
from app.gateway.trade_gateway import UnsupportedTradeType, submit_trade
|
||||
from app.repository.risk_repository import RiskRepository
|
||||
|
||||
router = APIRouter(prefix="/api/simulate", tags=["simulate"])
|
||||
|
||||
|
||||
def _repo() -> RiskRepository:
|
||||
"""审计仓储(deny 留痕用;测试 monkeypatch 点)。"""
|
||||
return RiskRepository()
|
||||
|
||||
|
||||
class TradeRequest(BaseModel):
|
||||
customer_id: str = Field(..., min_length=1)
|
||||
product_id: str = Field(..., min_length=1)
|
||||
@@ -28,11 +36,16 @@ class TradeRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post("/trade")
|
||||
def submit_trade_api(req: TradeRequest) -> dict:
|
||||
def submit_trade_api(req: TradeRequest, auth: AuthContext = Depends(get_auth_context)) -> dict:
|
||||
"""模拟交易(FR-1):适当性阻断或放行+引擎判定,返回 blocked + trade_id。"""
|
||||
if not (auth.has_role("risk_demo") or (auth.is_customer() and auth.customer_id == req.customer_id)):
|
||||
deny(
|
||||
auth, "AUTH_403_ROLE", _repo(),
|
||||
customer_id=req.customer_id, message="risk_demo or owner customer only",
|
||||
)
|
||||
try:
|
||||
return submit_trade(req.model_dump())
|
||||
except UnsupportedTradeType as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except LookupError as exc: # KeyError 亦为其子类;B6 统一 NotFoundError 时收敛为精确类型
|
||||
except LookupError as exc: # NotFoundError 亦为其子类;已统一(B6 评审 P3-5)
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
Reference in New Issue
Block a user