- Added `auth.py` for mock login and JWT issuance. - Introduced `chat.py` for handling chat requests with role-based access control. - Enhanced `main.py` to include new routers and middleware for tracing. - Implemented input validation in `input_guard.py` to prevent SQL injection. - Created repositories for managing agent sessions and audit logs. - Added exception handling for authorization errors. - Updated settings to include JWT configuration. - Introduced tests for authentication and input validation.
144 lines
4.1 KiB
Python
144 lines
4.1 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 = "https://idp.jinrong.dev"
|
|
AUDIENCE = "agent-gateway"
|
|
|
|
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",
|
|
],
|
|
"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"],
|
|
"STAFF-30002": ["risk_officer"],
|
|
"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"]
|
|
return DEFAULT_ROLES_BY_ACTOR.get(actor_id, ["analyst"])
|
|
|
|
|
|
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": "default",
|
|
}
|
|
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
|