2026-09-07 18:28:18 +08:00
|
|
|
|
"""宿主 AuthContext → 模块 AuthContext 适配器(S2 接缝,AL-09 接线用)。
|
|
|
|
|
|
|
2026-09-08 19:55:08 +08:00
|
|
|
|
状态:**AL-09 已接线(JWT 统一 + 适配器可用)**。
|
|
|
|
|
|
- `/api/auth/login` 与模块 API 共用 `settings.jwt_issuer` / `jwt_audience` 签发的 token;
|
|
|
|
|
|
- chat / risk 一律 `deps.get_auth_context` 验签;
|
|
|
|
|
|
- 若某宿主路由仍产出 `schemas.AuthContext`,经 `module_auth_from_host()` 转换后进入模块 Service。
|
2026-09-07 18:28:18 +08:00
|
|
|
|
|
|
|
|
|
|
设计取舍:
|
|
|
|
|
|
- **不 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
|
|
|
|
|
|
|
2026-09-09 18:32:00 +08:00
|
|
|
|
from typing import Any, NamedTuple
|
2026-09-07 18:28:18 +08:00
|
|
|
|
|
|
|
|
|
|
from app.api.deps import AuthContext
|
|
|
|
|
|
from app.utils.trace import set_trace
|
|
|
|
|
|
|
2026-09-09 18:32:00 +08:00
|
|
|
|
__all__ = ["from_host_auth", "perm_matches", "resolve_agent_type", "module_auth_from_host", "host_auth_for_customer_service", "HostAuthAdapterError"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _HostAuthShim(NamedTuple):
|
|
|
|
|
|
"""与宿主 schemas.AuthContext 同形(S2 反向接缝;不 import 宿主模块)。"""
|
|
|
|
|
|
|
|
|
|
|
|
sub: str
|
|
|
|
|
|
token_type: str
|
|
|
|
|
|
roles: list[str]
|
|
|
|
|
|
permissions: list[str]
|
|
|
|
|
|
tenant_id: str
|
|
|
|
|
|
trace_id: str
|
|
|
|
|
|
agent_type: str
|
|
|
|
|
|
jti: str
|
|
|
|
|
|
customer_id: str | None
|
2026-09-07 18:28:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-09-08 19:55:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def module_auth_from_host(host_ctx: Any, *, agent_type: str | None = None) -> AuthContext:
|
|
|
|
|
|
"""S2 接缝接线入口:宿主 AuthContext → 模块 AuthContext(含 trace 绑定)。"""
|
|
|
|
|
|
resolved = agent_type or resolve_agent_type(host_ctx)
|
|
|
|
|
|
return from_host_auth(host_ctx, agent_type=resolved, bind_trace=True)
|
2026-09-09 18:32:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def host_auth_for_customer_service(module_auth: AuthContext, *, trace_id: str) -> _HostAuthShim:
|
|
|
|
|
|
"""模块 AuthContext → 宿主 AuthContext 同形对象(客服 customer_service 专用)。
|
|
|
|
|
|
|
|
|
|
|
|
customer_service 仍使用 schemas.AuthContext 字段名(sub/trace_id/agent_type),
|
|
|
|
|
|
chat 模块线走 deps.AuthContext;本函数为 S2 反向接缝,仅 customer 分流调用。
|
|
|
|
|
|
"""
|
|
|
|
|
|
customer_id = module_auth.customer_id
|
|
|
|
|
|
if customer_id is None and module_auth.token_type == "customer":
|
|
|
|
|
|
customer_id = module_auth.actor_id
|
|
|
|
|
|
|
|
|
|
|
|
return _HostAuthShim(
|
|
|
|
|
|
sub=module_auth.actor_id,
|
|
|
|
|
|
token_type=module_auth.token_type,
|
|
|
|
|
|
roles=list(module_auth.roles),
|
|
|
|
|
|
permissions=list(module_auth.permissions),
|
|
|
|
|
|
tenant_id=module_auth.tenant_id or "default",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
agent_type="customer",
|
|
|
|
|
|
jti=module_auth.jti or "",
|
|
|
|
|
|
customer_id=customer_id,
|
|
|
|
|
|
)
|