"""鉴权拒绝留痕公共出口(手册 P-05 双写;T-04 评审 P1-2 收口)。 背景:越权留痕原本只在 API 层(api/deps._authz_audit)实现,对话 Tool 层 归属被拒时只落 agent_tool_call,全局鉴权审计维度缺失(输入防护台账查不到 对话链路的越权尝试)。本模块把该逻辑下沉到 utils,供两方共用: - API 层:deps.deny / _authz_audit(403) - 对话层:tool_service.run_tool 的 blocked 分支(AUTH_403_* 归属拒绝) 放 utils 而非 api 的原因:service 层不得反向依赖 api 层(分层约束)。 降级口径(与 T-02 审计一致):写库失败仅本地 logger.exception,不改变 拒绝语义——对话不因留痕故障中断,生产可切 fail-closed(待决,已登记)。 """ from __future__ import annotations import logging from typing import Any, Sequence from app.utils.trace import current_trace, new_trace logger = logging.getLogger(__name__) # 四 Agent 入口(input_guard_log.agent_type 的 ENUM 仅此四值;platform 跳过) AGENT_TYPES = ("customer", "advisor", "analyst", "risk") def record_authz_denial( risk_repo: Any, *, actor_id: str, code: str, roles: Sequence[str] | None = None, customer_id: str | None = None, agent_type: str = "risk", trace_id: str | None = None, ) -> None: """越权/拒绝留痕:audit_log(authz) + input_guard_log(四 Agent 内)。 agent_type 非四 Agent(如 platform 网关)时只写 audit_log—— input_guard_log.agent_type 是 ENUM,越界写会报数据截断(T-02 同口径)。 """ trace_id = trace_id or current_trace() or new_trace() try: risk_repo.insert_audit_log( { "trace_id": trace_id, "event_type": "authz", "agent_type": agent_type, "actor_id": actor_id or "anonymous", "customer_id": customer_id, "rule_id": None, "input_summary": {"roles": list(roles or []), "code": code}, "decision": "forbidden", "risk_score": None, "handler_id": None, "handler_result": None, "handler_comment": None, } ) if agent_type in AGENT_TYPES: risk_repo.insert_input_guard_log( trace_id=trace_id, agent_type=agent_type, actor_id=actor_id or "anonymous", guard_type="illegal_param", action="blocked", raw_excerpt=code, ) except Exception: logger.exception( "authz audit failed (degraded): code=%s agent=%s actor=%s", code, agent_type, actor_id, )