"""FastAPI dependencies for authentication, RBAC, and RLS context.""" from __future__ import annotations from enum import Enum from typing import Annotated, AsyncIterator, Optional import asyncpg from fastapi import Cookie, Depends, HTTPException, status from .auth_sessions import get_session from .config import Settings, get_settings from .db import acquire class Role(str, Enum): LEARNER = "learner" TEACHER = "teacher" ADMIN = "admin" class AIView(str, Enum): CLIENT = "client" COUNSELOR = "counselor" EVALUATOR = "evaluator" class Principal: """Authenticated human principal.""" def __init__( self, user_id: str, role: Role, cohort_ids: Optional[list[str]] = None, email: str = "", display_name: str = "", ) -> None: self.user_id = user_id self.role = role self.cohort_ids = cohort_ids or [] self.email = email self.display_name = display_name def get_settings_dep() -> Settings: return get_settings() async def get_current_principal( session_cookie: Annotated[Optional[str], Cookie(alias="__Host-vignette_sid")] = None, dev_session_cookie: Annotated[Optional[str], Cookie(alias="vignette_sid")] = None, ) -> Principal: """Restore the principal from the opaque HttpOnly browser session cookie.""" raw_cookie = session_cookie or (dev_session_cookie if get_settings().environment == "dev" else None) session = await get_session(raw_cookie) if session is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="not authenticated", ) try: role = Role(session.role) except ValueError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid session role", ) from exc return Principal( user_id=session.user_id, role=role, cohort_ids=session.cohort_ids, email=session.email, display_name=session.display_name, ) def require_role(*allowed: Role): """Role allowlist dependency factory.""" async def _checker( principal: Annotated[Principal, Depends(get_current_principal)], ) -> Principal: if principal.role not in allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"role {principal.role.value} not permitted", ) return principal return _checker async def db_for_human( principal: Annotated[Principal, Depends(get_current_principal)], ) -> AsyncIterator[asyncpg.Connection]: """Acquire a DB connection with the human RBAC context attached.""" async with acquire( role=principal.role.value, user_id=principal.user_id, cohort_ids=principal.cohort_ids, ) as conn: yield conn def db_for_ai_view(view: AIView): """Dependency factory for AI-side RLS visibility context.""" async def _provider() -> AsyncIterator[asyncpg.Connection]: async with acquire(ai_view=view.value, ai_context=True) as conn: yield conn return _provider CurrentPrincipal = Annotated[Principal, Depends(get_current_principal)] HumanDB = Annotated[asyncpg.Connection, Depends(db_for_human)]