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:
2026-09-06 18:58:48 +08:00
parent e3fcf28b27
commit 3e40a2bb49
7 changed files with 208 additions and 69 deletions
+35 -2
View File
@@ -126,7 +126,10 @@ def client(env, monkeypatch):
repo, engine = env
monkeypatch.setattr(risk_api, "_repo", lambda: repo)
monkeypatch.setattr(risk_api, "CoreReadOnlyRepository", lambda: CoreReadOnlyRepository(engine=engine))
monkeypatch.setattr(risk_api, "CoreReadOnlyRepository", lambda: CoreReadOnlyRepository(engine=engine))
# 401/越权审计经 deps 内 RiskRepository 兜底构造,统一注入 sqlite(B6 评审 P3-4)
from app.api import deps as deps_mod
monkeypatch.setattr(deps_mod, "RiskRepository", lambda: repo)
app = FastAPI()
app.include_router(risk_router)
app.add_exception_handler(PermissionDenied, permission_denied_handler)
@@ -322,5 +325,35 @@ def test_assert_customer_access_scope_denial(env):
auth = AuthContext(actor_id="STAFF-40001", roles=["compliance"])
with pytest.raises(PermissionDenied) as ei:
assert_customer_access(auth, "CUST-1001", risk_repo=repo)
assert_customer_access(
auth, "CUST-1001", core_ro=CoreReadOnlyRepository(engine=env[1]), risk_repo=repo
)
assert ei.value.code == "AUTH_403_SCOPE"
def test_authz_denials_are_audited(client, env):
"""B6 评审 P1-1:403/401 全部留痕(AUTH_403_ROLE / AUTH_401_MISSING_DEBUG_HEADERS)。"""
repo, engine = env
assert client.get("/api/risk/alerts").status_code == 401 # 无 debug 头
assert client.post("/api/risk/aml/scan", headers=ADV_01).status_code == 403
assert client.post(
"/api/risk/alerts/ALT-E1/handle",
json={"handler_result": "confirmed_normal"},
headers=COMPLIANCE,
).status_code == 403
assert _counts(engine, "audit_log", "decision='unauthenticated' AND input_summary LIKE '%AUTH_401%'") == 1
assert _counts(engine, "audit_log", "decision='forbidden' AND input_summary LIKE '%AUTH_403_ROLE%'") == 2
def test_multi_role_combinations_fixed_behavior(client):
"""B6 评审 P3-1:多角色口径固化——并集权限、归属 fail-closed。"""
# customer+compliance:借 compliance 角色看 aml 台账(审计类跨客户,固化允许)
r = client.get("/api/risk/alerts", headers=_h("customer,compliance", "CUST-1001"))
assert r.status_code == 200 and r.json()["items"][0]["alert_type"] == "aml"
# customer+advisor 查非本人客户:customer 分支 deny 即终止(最窄范围 fail-closed)
r = client.post(
"/api/risk/suitability/check",
json={"customer_id": "CUST-1001", "product_id": "PROD-161725"},
headers=_h("customer,advisor", "CUST-3001"),
)
assert r.status_code == 403 and "AUTH_403_NOT_OWNER" in r.json()["detail"]