import hashlib import secrets from datetime import datetime, timedelta, timezone from typing import Any from uuid import UUID, uuid4 from jose import JWTError, jwt from src.core.exceptions import ForbiddenException, UnauthorizedException from src.domain.entities import UserRole from src.infrastructure.config.settings import settings def hash_password(password: str) -> str: import bcrypt as _bcrypt return _bcrypt.hashpw(password.encode("utf-8"), _bcrypt.gensalt()).decode("utf-8") def verify_password(plain_password: str, hashed_password: str) -> bool: import bcrypt as _bcrypt return _bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) def create_access_token(subject: str | UUID, role: UserRole, scopes: list[str], extra_claims: dict | None = None) -> str: now = datetime.now(timezone.utc) expires = now + timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES) claims: dict[str, Any] = { "sub": str(subject), "role": role.value, "scopes": scopes, "iss": settings.JWT_ISSUER, "iat": now, "exp": expires, "type": "access", "jti": str(uuid4()), } if extra_claims: claims.update(extra_claims) return jwt.encode(claims, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) def create_refresh_token(subject: str | UUID) -> str: now = datetime.now(timezone.utc) expires = now + timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS) claims: dict[str, Any] = { "sub": str(subject), "iss": settings.JWT_ISSUER, "iat": now, "exp": expires, "type": "refresh", "jti": str(uuid4()), } return jwt.encode(claims, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) def decode_token(token: str) -> dict[str, Any]: try: payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM], issuer=settings.JWT_ISSUER) return payload except JWTError as e: raise UnauthorizedException(message=f"Invalid token: {e}") from e def validate_token_type(payload: dict[str, Any], expected_type: str = "access") -> dict[str, Any]: if payload.get("type") != expected_type: raise UnauthorizedException(message=f"Expected {expected_type} token, got {payload.get('type')}") return payload class TokenData: def __init__(self, payload: dict[str, Any]): self.subject: str = payload.get("sub", "") self.role: UserRole = UserRole(payload.get("role", "viewer")) self.scopes: list[str] = payload.get("scopes", []) self.issuer: str = payload.get("iss", "") self.token_type: str = payload.get("type", "access") self.jti: str = payload.get("jti", "") self.exp: datetime | None = None if exp := payload.get("exp"): self.exp = datetime.fromtimestamp(exp, tz=timezone.utc) class RBACGuard: ROLE_HIERARCHY: dict[UserRole, set[UserRole]] = { UserRole.SUPERADMIN: {UserRole.SUPERADMIN, UserRole.ADMIN, UserRole.OPERATOR, UserRole.VIEWER, UserRole.API_CLIENT}, UserRole.ADMIN: {UserRole.ADMIN, UserRole.OPERATOR, UserRole.VIEWER, UserRole.API_CLIENT}, UserRole.OPERATOR: {UserRole.OPERATOR, UserRole.VIEWER}, UserRole.VIEWER: {UserRole.VIEWER}, UserRole.API_CLIENT: {UserRole.API_CLIENT, UserRole.VIEWER}, } @classmethod def has_role(cls, token_data: TokenData, required_role: UserRole) -> bool: allowed = cls.ROLE_HIERARCHY.get(token_data.role, set()) return required_role in allowed @classmethod def require_role(cls, token_data: TokenData, required_role: UserRole) -> None: if not cls.has_role(token_data, required_role): raise ForbiddenException(required_role=required_role.value) @classmethod def has_scope(cls, token_data: TokenData, required_scope: str) -> bool: if "*" in token_data.scopes: return True return required_scope in token_data.scopes @classmethod def require_scope(cls, token_data: TokenData, required_scope: str) -> None: if not cls.has_scope(token_data, required_scope): raise ForbiddenException(message=f"Scope '{required_scope}' required") def generate_api_key() -> tuple[str, str, str]: raw_key = secrets.token_urlsafe(settings.API_KEY_LENGTH) prefix = raw_key[:8] key_hash = hashlib.sha256(raw_key.encode()).digest() return raw_key, prefix, key_hash def hash_api_key(raw_key: str) -> bytes: return hashlib.sha256(raw_key.encode()).digest() def verify_api_key(raw_key: str, stored_hash: bytes) -> bool: return hashlib.sha256(raw_key.encode()).digest() == stored_hash