"""deps.AuthContext → 数据分析 Agent 内部视图(S3 接缝)。""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any from app.api.deps import AuthContext as DepsAuthContext from app.utils.trace import current_trace # Demo 四角色 + ops 预留;问数线宽于 chat 矩阵(customer 可进) ROLE_DATA_DOMAIN: dict[str, str] = { "customer": "self", "advisor": "assigned", "analyst": "full", "risk_officer": "risk", "ops": "aggregate", } _ROLE_PRIORITY = ("customer", "advisor", "analyst", "risk_officer", "ops") class AnalystAuthError(Exception): def __init__(self, error_code: str, message: str) -> None: super().__init__(message) self.error_code = error_code self.message = message @dataclass class AnalystAuthContext: """分析线内部身份(subject_id 对齐 deps.actor_id)。""" subject_id: str token_type: str roles: list[str] permissions: list[str] = field(default_factory=list) customer_id: str | None = None trace_id: str = "" agent_type: str = "analyst" def has_role(self, role: str) -> bool: return role in self.roles def analyst_auth_from_deps( ctx: DepsAuthContext, *, trace_id: str | None = None, ) -> AnalystAuthContext: return AnalystAuthContext( subject_id=ctx.actor_id, token_type=ctx.token_type, roles=list(ctx.roles), permissions=list(ctx.permissions), customer_id=ctx.customer_id, trace_id=trace_id or current_trace() or "", ) def assert_analyst_query_access(ctx: AnalystAuthContext) -> str: """问数入口鉴权:返回 sql_guard 数据域 key。失败抛 AnalystAuthError。""" if ctx.token_type == "customer" or "customer" in ctx.roles: if not ctx.customer_id: raise AnalystAuthError("AUTH_403_ROLE", "客户 token 缺少 customer_id") return "self" if ctx.token_type != "staff": raise AnalystAuthError("AUTH_403_ROLE", "数据分析问数需有效登录身份") for role in _ROLE_PRIORITY: if role in ctx.roles and role in ROLE_DATA_DOMAIN and role != "customer": return ROLE_DATA_DOMAIN[role] raise AnalystAuthError("AUTH_403_ROLE", "当前角色无权使用数据分析问数") def resolve_analyst_scope(ctx: AnalystAuthContext, domain: str, repo: Any) -> list[str]: """按域解析 customer_id 白名单(供 sql_guard)。""" if domain == "self": cid = ctx.customer_id or ctx.subject_id return [cid] if cid else [] if domain == "assigned": return repo.resolve_advisor_scope(ctx.subject_id) return []