from __future__ import annotations from dataclasses import dataclass, field from hashlib import sha256 from hmac import compare_digest from collections.abc import Iterable from typing import Protocol, runtime_checkable TASKS_SUBMIT_SCOPE = "tasks:submit" TASKS_READ_SCOPE = "tasks:read" POOL_READ_SCOPE = "pool:read" PLUGINS_READ_SCOPE = "plugins:read" PLUGINS_ADMIN_SCOPE = "plugins:admin" @dataclass(frozen=True) class Principal: id: str = "anonymous" scopes: frozenset[str] = field(default_factory=frozenset) host_id: str | None = None def has_scope(self, scope: str) -> bool: return "*" in self.scopes or scope in self.scopes def require_host(self, requested_host_id: str) -> None: if self.host_id is None: raise HostPrincipalRequiredError("a host-bound principal is required") if self.host_id != requested_host_id: raise HostIdentityMismatchError( "authenticated host cannot act for the requested host" ) class HostAuthorizationError(PermissionError): pass class HostPrincipalRequiredError(HostAuthorizationError): pass class HostIdentityMismatchError(HostAuthorizationError): pass @runtime_checkable class AuthProvider(Protocol): def authenticate(self, request: object) -> Principal | None: ... class NullAuthProvider: """Explicit insecure-development provider with unrestricted scope.""" def authenticate(self, request: object) -> Principal | None: return Principal(scopes=frozenset({"*"})) @dataclass(frozen=True) class BearerCredential: principal_id: str token: str = field(repr=False) scopes: frozenset[str] = field(default_factory=frozenset) host_id: str | None = None def __post_init__(self) -> None: if not self.principal_id.strip(): raise ValueError("bearer credential principal_id must not be empty") if not self.token: raise ValueError("bearer credential token must not be empty") if self.host_id is not None and not self.host_id.strip(): raise ValueError("bearer credential host_id must not be empty") @dataclass(frozen=True) class _StoredBearerCredential: principal: Principal token_digest: bytes = field(repr=False) class ConfiguredBearerAuthProvider: """Authenticate configured bearer tokens without exposing credential text.""" def __init__(self, credentials: list[BearerCredential]) -> None: self._credentials = tuple( _StoredBearerCredential( principal=Principal( id=credential.principal_id, scopes=frozenset(credential.scopes), host_id=credential.host_id, ), token_digest=_token_digest(credential.token), ) for credential in credentials ) def authenticate(self, request: object) -> Principal | None: token = _extract_bearer_token(request) if token is None: return None candidate_digest = _token_digest(token) matched_principal: Principal | None = None for credential in self._credentials: if compare_digest(candidate_digest, credential.token_digest): matched_principal = credential.principal return matched_principal def create_auth_provider( credentials: Iterable[BearerCredential], *, allow_insecure_anonymous: bool, ) -> AuthProvider: configured = list(credentials) if configured: return ConfiguredBearerAuthProvider(configured) if allow_insecure_anonymous: return NullAuthProvider() return ConfiguredBearerAuthProvider([]) def _extract_bearer_token(request: object) -> str | None: headers = getattr(request, "headers", None) if headers is None: return None authorization = headers.get("authorization") if not isinstance(authorization, str): return None scheme, separator, token = authorization.partition(" ") if not separator or scheme.lower() != "bearer" or not token: return None return token def _token_digest(token: str) -> bytes: return sha256(token.encode("utf-8")).digest()