- 新增 app/api、app/service 数据分析 Agent 全套服务与接口 - schemas.py 重构为 schemas 包(analyst schema) - 新增 SQL 防注入、guardrail、缓存、字典、LLM 等服务 - 新增 tests 测试套件与 scripts/dev、scripts/setup 脚本 - 补充需求规格、架构说明书、开发清单、表设计等文档
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""鉴权:Mock JWT(HS256,开发用)+ AuthContext + RBAC 角色判定。
|
||
|
||
生产对齐 docs/项目框架设计/技术选型和版本/02-JWT-RBAC鉴权手册.md(RS256 + IdP);
|
||
本模块只覆盖数据分析 Agent 开发/联调所需的最小身份与角色能力。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Any
|
||
|
||
from jose import jwt, JWTError
|
||
|
||
from app.config.settings import settings
|
||
|
||
ALGORITHM = "HS256"
|
||
|
||
|
||
class AuthError(Exception):
|
||
"""鉴权失败,携带错误码(对齐 JWT 手册 §10)。"""
|
||
|
||
def __init__(self, error_code: str, message: str) -> None:
|
||
super().__init__(message)
|
||
self.error_code = error_code
|
||
self.message = message
|
||
|
||
|
||
@dataclass
|
||
class AuthContext:
|
||
"""注入给业务层的最小身份上下文。"""
|
||
|
||
subject_id: str
|
||
token_type: str
|
||
roles: list[str]
|
||
permissions: list[str] = field(default_factory=list)
|
||
staff_type: str = ""
|
||
trace_id: str = ""
|
||
agent_type: str = "analyst"
|
||
|
||
def has_role(self, role: str) -> bool:
|
||
return role in self.roles
|
||
|
||
def has_perm(self, perm: str) -> bool:
|
||
return perm in self.permissions
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"subject_id": self.subject_id,
|
||
"token_type": self.token_type,
|
||
"roles": self.roles,
|
||
"permissions": self.permissions,
|
||
"staff_type": self.staff_type,
|
||
"trace_id": self.trace_id,
|
||
"agent_type": self.agent_type,
|
||
}
|
||
|
||
|
||
def create_dev_token(
|
||
subject_id: str,
|
||
roles: list[str],
|
||
staff_type: str = "",
|
||
permissions: list[str] | None = None,
|
||
expires_hours: int | None = None,
|
||
) -> str:
|
||
"""开发/联调用:签发员工 Token(HS256)。"""
|
||
ttl = expires_hours or settings.jwt_token_ttl_hours
|
||
claims: dict[str, Any] = {
|
||
"sub": subject_id,
|
||
"token_type": "staff",
|
||
"roles": roles,
|
||
"staff_type": staff_type,
|
||
"permissions": permissions or [],
|
||
}
|
||
return jwt.encode(claims, settings.jwt_dev_secret, algorithm=ALGORITHM)
|
||
|
||
|
||
def verify_token(token: str) -> AuthContext:
|
||
"""验签并解析为 AuthContext;失败抛 AuthError(401)。"""
|
||
try:
|
||
claims = jwt.decode(token, settings.jwt_dev_secret, algorithms=[ALGORITHM])
|
||
except JWTError as exc:
|
||
raise AuthError("AUTH_401_INVALID_TOKEN", "token 无效或已过期") from exc
|
||
return AuthContext(
|
||
subject_id=str(claims.get("sub", "")),
|
||
token_type=str(claims.get("token_type", "staff")),
|
||
roles=list(claims.get("roles", [])),
|
||
permissions=list(claims.get("permissions", [])),
|
||
staff_type=str(claims.get("staff_type", "")),
|
||
)
|
||
|
||
|
||
# 数据分析 Agent 允许进入的员工角色(需求规格 §2.1 角色矩阵)
|
||
ANALYST_ROLES = {"analyst", "advisor", "risk_officer", "ops"}
|
||
|
||
# 角色 → 需求规格 §2.1 的数据域
|
||
ROLE_DATA_DOMAIN = {
|
||
"analyst": "full", # 全量 + 全明细 + 敏感列可见
|
||
"advisor": "assigned", # 仅名下客户 + 明细 + 脱敏
|
||
"risk_officer": "risk", # 台账全量 + 客户只读 + 客户脱敏
|
||
"ops": "aggregate", # 无客户维度 + 仅聚合
|
||
}
|
||
|
||
|
||
def assert_analyst_access(ctx: AuthContext) -> str:
|
||
"""校验角色可进入数据分析 Agent,返回数据域。失败抛 AuthError(403)。"""
|
||
if ctx.token_type != "staff":
|
||
raise AuthError("AUTH_403_ROLE", "数据分析 Agent 仅限内部员工使用")
|
||
for role in ctx.roles:
|
||
if role in ANALYST_ROLES:
|
||
return ROLE_DATA_DOMAIN[role]
|
||
raise AuthError("AUTH_403_ROLE", "当前角色无权使用数据分析 Agent")
|