"""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, Request, status from .auth_types import AccountStatus 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, admin_access: bool = False, super_admin: bool = False, account_status: AccountStatus = "approved", cohort_ids: Optional[list[str]] = None, email: str = "", display_name: str = "", consent_at: float | None = None, profile_completed_at: float | None = None, ) -> None: self.user_id = user_id self.role = role self.admin_access = admin_access self.super_admin = super_admin self.account_status = account_status self.cohort_ids = cohort_ids or [] self.email = email self.display_name = display_name self.consent_at = consent_at self.profile_completed_at = profile_completed_at def can_access_role(self, role: Role) -> bool: if self.role == role: return True if self.super_admin: return True if role == Role.ADMIN: return self.admin_access return False def with_role(self, role: Role) -> "Principal": return Principal( user_id=self.user_id, role=role, admin_access=self.admin_access, super_admin=self.super_admin, account_status=self.account_status, cohort_ids=list(self.cohort_ids), email=self.email, display_name=self.display_name, consent_at=self.consent_at, profile_completed_at=self.profile_completed_at, ) def get_settings_dep() -> Settings: return get_settings() async def get_current_principal( request: Request, 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 if session.account_status != "approved" and request.url.path != "/auth/me": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"account_{session.account_status}", ) return Principal( user_id=session.user_id, role=role, admin_access=session.admin_access, super_admin=session.super_admin, account_status=session.account_status, cohort_ids=session.cohort_ids, email=session.email, display_name=session.display_name, consent_at=session.consent_at, profile_completed_at=session.profile_completed_at, ) def require_role(*allowed: Role): """Role allowlist dependency factory.""" async def _checker( principal: Annotated[Principal, Depends(get_current_principal)], ) -> Principal: if principal.role in allowed: return principal if principal.super_admin: effective = Role.ADMIN if Role.ADMIN in allowed else allowed[0] return principal.with_role(effective) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"role {principal.role.value} not permitted", ) return _checker def require_admin_access(): """기본 역할과 별개로 관리자 권한을 가진 사용자만 통과시킨다.""" async def _checker( principal: Annotated[Principal, Depends(get_current_principal)], ) -> Principal: if not principal.admin_access: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="admin access required", ) 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)]