- Updated `RiskListAccess` and `ThresholdWriteAccess` to enforce access control in the risk repository and threshold repository, ensuring only authorized roles can perform sensitive operations. - Introduced new methods in `RiskRepository` for counting pending alerts and listing alerts with access checks, improving data security and compliance. - Enhanced the `chat.py` and `deps.py` files to integrate compliance roles into the risk management matrix, allowing for more granular access control. - Updated documentation to reflect the new testing baseline of 825 passed tests, indicating improved stability and functionality across the application. This update significantly strengthens the risk management capabilities, ensuring robust access control and compliance with organizational policies.
154 lines
4.4 KiB
Python
154 lines
4.4 KiB
Python
"""JWT 签发与校验(开发环境 HS256)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Any
|
|
|
|
from jose import JWTError, jwt
|
|
|
|
from app.config.settings import settings
|
|
from app.model.schemas import TokenType
|
|
from app.utils.exceptions import UnauthorizedError
|
|
|
|
ISSUER = settings.jwt_issuer
|
|
AUDIENCE = settings.jwt_audience
|
|
|
|
ROLE_PERMISSIONS: dict[str, list[str]] = {
|
|
"customer": [
|
|
"agent:customer:chat",
|
|
"profile:l1:read",
|
|
"profile:l1:write",
|
|
"core:holding:read:self",
|
|
],
|
|
"advisor": [
|
|
"agent:advisor:chat",
|
|
"profile:l1:read",
|
|
"profile:l2:read",
|
|
"profile:l2:write",
|
|
"profile:l3:read",
|
|
"core:holding:read:assigned",
|
|
],
|
|
"analyst": [
|
|
"agent:analyst:chat",
|
|
"profile:l1:read",
|
|
"profile:l2:read",
|
|
"profile:l3:read",
|
|
"core:holding:read:scoped",
|
|
"sql:execute:readonly",
|
|
"risk:alert:read",
|
|
],
|
|
"risk_officer": [
|
|
"agent:risk:chat",
|
|
"profile:l1:read",
|
|
"profile:l2:read",
|
|
"profile:l3:read",
|
|
"profile:l3:write",
|
|
"risk:alert:write",
|
|
"risk:suitability:write",
|
|
"core:holding:read:all",
|
|
],
|
|
"risk_manager": [
|
|
"agent:risk:chat",
|
|
"profile:l1:read",
|
|
"profile:l2:read",
|
|
"profile:l3:read",
|
|
"risk:alert:read",
|
|
"core:holding:read:all",
|
|
],
|
|
"compliance": ["audit:read:all", "agent:advisor:audit", "compliance:hit:read"],
|
|
"ops": ["agent:advisor:stats", "audit:read:aggregated"],
|
|
"service_risk": [
|
|
"agent:risk:suitability_check",
|
|
"risk:suitability:write",
|
|
"profile:l1:read",
|
|
"profile:l2:read",
|
|
"audit:write",
|
|
],
|
|
}
|
|
|
|
DEFAULT_ROLES_BY_ACTOR: dict[str, list[str]] = {
|
|
"STAFF-10086": ["advisor"],
|
|
"STAFF-10087": ["advisor"],
|
|
"STAFF-10088": ["advisor"],
|
|
"STAFF-10089": ["advisor"],
|
|
"STAFF-10090": ["advisor"],
|
|
"STAFF-10091": ["advisor", "compliance"],
|
|
"STAFF-20001": ["analyst"],
|
|
"STAFF-20002": ["analyst"],
|
|
"STAFF-30001": ["risk_officer", "risk_demo"],
|
|
"STAFF-30002": ["risk_officer", "risk_demo"],
|
|
"STAFF-31001": ["risk_manager"],
|
|
"STAFF-40001": ["compliance"],
|
|
"STAFF-40002": ["compliance"],
|
|
"STAFF-50001": ["ops"],
|
|
"CUST-9527": ["customer"],
|
|
"CUST-1001": ["customer"],
|
|
"CUST-1002": ["customer"],
|
|
"CUST-1010": ["customer"],
|
|
}
|
|
|
|
|
|
def infer_roles(actor_id: str, token_type: TokenType, roles: list[str] | None) -> list[str]:
|
|
if roles:
|
|
return roles
|
|
if token_type == "customer":
|
|
return ["customer"]
|
|
# fail-closed:未知 actor 不给默认 analyst,避免越权(AL-09 / 宿主 P1)
|
|
return DEFAULT_ROLES_BY_ACTOR.get(actor_id, [])
|
|
|
|
|
|
def permissions_for_roles(roles: list[str]) -> list[str]:
|
|
perms: list[str] = []
|
|
seen: set[str] = set()
|
|
for role in roles:
|
|
for perm in ROLE_PERMISSIONS.get(role, []):
|
|
if perm not in seen:
|
|
seen.add(perm)
|
|
perms.append(perm)
|
|
return perms
|
|
|
|
|
|
def issue_token(
|
|
actor_id: str,
|
|
token_type: TokenType,
|
|
roles: list[str] | None = None,
|
|
) -> tuple[str, int]:
|
|
role_list = infer_roles(actor_id, token_type, roles)
|
|
now = datetime.now(UTC)
|
|
expires = now + timedelta(hours=settings.jwt_dev_expire_hours)
|
|
payload: dict[str, Any] = {
|
|
"iss": ISSUER,
|
|
"sub": actor_id,
|
|
"aud": AUDIENCE,
|
|
"exp": int(expires.timestamp()),
|
|
"iat": int(now.timestamp()),
|
|
"jti": str(uuid.uuid4()),
|
|
"token_type": token_type,
|
|
"roles": role_list,
|
|
"permissions": permissions_for_roles(role_list),
|
|
"tenant_id": "TENANT-001",
|
|
}
|
|
if token_type == "customer":
|
|
payload["customer_id"] = actor_id
|
|
if "advisor" in role_list and token_type == "staff":
|
|
payload["advisor_id"] = actor_id
|
|
|
|
token = jwt.encode(payload, settings.jwt_dev_secret, algorithm=settings.jwt_dev_algorithm)
|
|
return token, settings.jwt_dev_expire_hours * 3600
|
|
|
|
|
|
def decode_token(token: str) -> dict[str, Any]:
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
settings.jwt_dev_secret,
|
|
algorithms=[settings.jwt_dev_algorithm],
|
|
audience=AUDIENCE,
|
|
issuer=ISSUER,
|
|
)
|
|
except JWTError as exc:
|
|
raise UnauthorizedError("Token 无效或已过期", error_code="AUTH_401_INVALID") from exc
|
|
return payload
|