"""FastAPI dependencies for JWT-based identity and authorization.""" import logging import re import uuid from typing import Any, Optional from fastapi import HTTPException, Request from python_pdp_sdk import ( ForwardKwargsGetter, UnauthenticatedException, UnauthorizedException, ) from contributor.api import datasources logger = logging.getLogger(__name__) USER_METADATA_CLAIM = "https://grass.theorchard.com/user_metadata" PROFILES_CLAIM = "https://grass.theorchard.com/profiles" OA_USER_ID_RE = re.compile(r"^oa:\d+$") def _require_token(request: Request) -> dict[str, Any]: if "token" not in request.scope: logger.error("JWT not decoded by JWTAuthenticationMiddleware") raise HTTPException(status_code=401, detail="Unauthenticated") return request.scope["token"] def identity_uuid_from_scope(request: Request) -> Optional[uuid.UUID]: """Return the authenticated principal's identity UUID from the JWT.""" token = _require_token(request) user_metadata = token.get(USER_METADATA_CLAIM, {}) raw = user_metadata.get("orchardIdentityId") if not raw: logger.error("Missing identity UUID claim") raise HTTPException(status_code=401, detail="Missing identity UUID") try: return uuid.UUID(raw, version=4) except ValueError: logger.error("Malformed identity UUID: %s", raw) raise HTTPException(status_code=401, detail="Malformed identity UUID") def profiles_from_scope(request: Request) -> list[dict[str, Any]]: """Return the authenticated principal's profiles from the JWT.""" token = _require_token(request) return token.get(PROFILES_CLAIM, []) def label_profile_ids_from_profiles(profiles: list[dict[str, Any]]) -> list[int]: """Extract all LabelProfile profileIds from the JWT profiles claim.""" return [ p["profile_id"] for p in profiles if p.get("profile_type") == "LabelProfile" and "profile_id" in p ] def is_authorized( action: str, resource_type: str, tenant_uuid: uuid.UUID, tenant_type: str = "account", resource_id: int = 0, ) -> bool: """Check authorization via ows-pdp for the given action and resource.""" authorization_backend = datasources.get_authorization_backend() try: return authorization_backend.is_authorized( action=action, resource_id=resource_id, resource_type=resource_type, resource_getter=ForwardKwargsGetter(), tenant={ "tenant_type": tenant_type, "tenant_uuid": str(tenant_uuid), }, ) except UnauthenticatedException: raise HTTPException(status_code=401, detail="Unauthenticated") except UnauthorizedException: raise HTTPException(status_code=403, detail="Forbidden") def is_oa_user(request: Request) -> bool: """Return True if the Orchard-User-Id header matches 'oa:' followed by digits.""" header_value: Optional[str] = request.headers.get("Orchard-User-Id") return header_value is not None and bool(OA_USER_ID_RE.match(header_value))