"""宿主 AuthContext → 模块 AuthContext 适配器(S2 接缝,AL-09 接线用)。 状态:**预制件,当前未接线**。模块内部继续直接使用 `app.api.deps.get_auth_context`。 AL-09 合并 main 后,在模块 API 入口把宿主的 `AuthContext` 经 `from_host_auth` 转换即可, 模块其余代码零改动。 设计取舍: - **不 import 宿主模块**(main 的 `app/model/schemas.py` 等),改用鸭子类型读取字段, 保证本文件在当前分支(宿主文件尚不存在)也能被导入与单测; - 只做字段映射与语义补齐,不做任何权限判定(判定仍在 `deps.py` 的矩阵内); - 权限原样透传,另提供 `perm_matches` 兼容宿主的 `前缀:*` 通配语义, 避免模块侧 `has_permission` 精确匹配漏判通配权限。 字段映射(宿主 → 模块): sub → actor_id (字段名差异,模块全量代码依赖 actor_id) trace_id → contextvar 绑定 (模块不存字段,走 utils.trace.current_trace) agent_type → 不入模,仅返回 (模块按 agent_type 单独传参做准入判定) roles/permissions/tenant_id/jti/customer_id → 原样透传 """ from __future__ import annotations from typing import Any from app.api.deps import AuthContext from app.utils.trace import set_trace __all__ = ["from_host_auth", "perm_matches", "HostAuthAdapterError"] class HostAuthAdapterError(ValueError): """宿主上下文字段缺失且无法安全兜底时抛出(fail-closed,不静默降级)。""" def perm_matches(perm: str, permissions: list[str] | tuple[str, ...]) -> bool: """权限匹配:精确命中,或命中宿主的 `前缀:*` 通配写法。 例:`risk:alert:read` 在权限集含 `risk:*` 时返回 True。 """ if not perm: return False if perm in permissions: return True prefix = perm.split(":", 1)[0] return any(p == f"{prefix}:*" or p.endswith(":*") and p.startswith(f"{prefix}:") for p in permissions) def from_host_auth( host_ctx: Any, *, agent_type: str | None = None, bind_trace: bool = True, ) -> AuthContext: """把宿主 AuthContext 转成模块 AuthContext。 Args: host_ctx: 宿主的 `AuthContext`(含 sub/roles/token_type/... 的任意对象)。 agent_type: 显式指定请求目标 agent;缺省时取宿主的 `agent_type` 字段。 bind_trace: 是否把宿主的 `trace_id` 绑定到模块 trace contextvar。 Raises: HostAuthAdapterError: 连主体标识(sub/actor_id)都取不到时抛出。 """ actor_id = getattr(host_ctx, "sub", None) or getattr(host_ctx, "actor_id", None) if not actor_id: # fail-closed:拿不到主体就拒绝,不匿名放行 raise HostAuthAdapterError("宿主 AuthContext 缺少主体标识(sub/actor_id),拒绝放行") roles = list(getattr(host_ctx, "roles", None) or []) permissions = list(getattr(host_ctx, "permissions", None) or []) token_type = getattr(host_ctx, "token_type", None) or "staff" customer_id = getattr(host_ctx, "customer_id", None) # customer 类 token 若未显式带 customer_id,回退为主体自身 if customer_id is None and token_type == "customer": customer_id = actor_id if bind_trace: trace_id = getattr(host_ctx, "trace_id", None) if trace_id: set_trace(str(trace_id)) return AuthContext( actor_id=str(actor_id), roles=roles, customer_id=customer_id, token_type=str(token_type), permissions=permissions, tenant_id=getattr(host_ctx, "tenant_id", None), jti=getattr(host_ctx, "jti", None), ) def resolve_agent_type(host_ctx: Any, default: str = "risk") -> str: """取请求目标 agent 类型;宿主无该字段时回退 default。""" value = getattr(host_ctx, "agent_type", None) return str(value) if value else default