Files
group_xinghuo_jinrong/app/gateway/jwt_service.py
T
zhanghongyu_0626 5c18164d34 fix(trade): close CT-001 P0/P1/L1 and align customer-trade E2E
Dev login no longer honors injected roles; self-service simulate/convert
blocks R4 disclosure grades like the UI and chat already do. Reuse of a
convert idempotency key with a different body returns 409. CT6 redeem qty
is derived from T+2 lots instead of a fixed 35000; CT7-05 and CT10-07
assertions follow. Update project memory and v1.1 post-fix test artifacts.
2026-09-13 20:04:41 +08:00

179 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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",
"advisor:workspace",
"compliance:check",
"template:read",
"market_alert:read",
"market_alert:generate",
"market_alert:feedback",
"kyc:create",
"kyc:chat",
"kyc:complete",
"allocation:create",
"copy:track",
"dashboard:personal",
"guard:check",
],
"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",
"admin:all",
"compliance:check",
"compliance:rule:write",
"template:write",
"audit:read",
"dashboard:global",
"guard:check",
],
"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-20003": ["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]:
# CT-001 P0:dev 登录不得信任调用方传入的 roles(防提权);仅白名单表定角色。
_ = roles
if token_type == "customer":
mapped = DEFAULT_ROLES_BY_ACTOR.get(actor_id)
return mapped if mapped else ["customer"]
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