vignette/apps/api/app/deps.py
Yun Chan c57c34ade6 feat(api): DB off degraded 기동(store 폴백) + dev 인증 우회 — 풀 API 라이브 E2E 통과
- main lifespan: init_pool 실패해도 store 인메모리 폴백으로 degraded 기동
- deps: environment=dev면 쿠키 없어도 더미 학습자(로컬 라이브). prod는 401 유지
- 실증: POST /sessions(201) → turn → 서연 client_reply, safety_flagged=false
2026-06-25 23:51:59 +09:00

121 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""의존성 — RBAC × visible_to 정보비대칭 게이트.
2-레이어 강제 (설계서 §4.1, F-30):
레이어1 = AI 정보비대칭: current_ai_view (CLIENT_AI 분기엔 CCD/정답 로드 코드경로 자체 부재)
레이어2 = 인간 RBAC×cohort: current_role (RLS DB 레벨 방어선)
인간이 turn 읽을 때 두 게이트 AND. 여기선 요청 컨텍스트 추출 + DB 세션변수 주입 계약만 제공.
NOTE: 실제 세션/쿠키 검증은 auth.py BFF + Redis 세션 구현 시 완성(현재 스텁).
"""
from __future__ import annotations
from enum import Enum
from typing import Annotated, AsyncIterator, Optional
import asyncpg
from fastapi import Cookie, Depends, HTTPException, status
from .config import Settings, get_settings
from .db import acquire
# ── 인간 역할 (RBAC) ────────────────────────────────────
class Role(str, Enum):
LEARNER = "learner" # 본인 세션만 (/learn)
TEACHER = "teacher" # 담당 코호트 전체 열람+검수 (/teach)
ADMIN = "admin" # 전부 + 교수활동 감사 (/admin)
# ── AI 뷰 (정보비대칭, current_ai_view enum) ────────────
class AIView(str, Enum):
CLIENT = "client" # 가상내담자 AI — CCD/정답/점수 절대 비노출
COUNSELOR = "counselor" # 상담사 AI(보조) — 표면 대화만, DSM 차단
EVALUATOR = "evaluator" # 평가 AI — 전부 봄 (학습자엔 비노출)
class Principal:
"""인증된 요청 주체. 인간 role + (선택) cohort 범위."""
def __init__(
self,
user_id: str,
role: Role,
cohort_ids: Optional[list[str]] = None,
) -> None:
self.user_id = user_id
self.role = role
self.cohort_ids = cohort_ids or []
def get_settings_dep() -> Settings:
return get_settings()
async def get_current_principal(
# __Host- HttpOnly 쿠키 (config.cookie_name). 브라우저엔 토큰 미노출.
session_cookie: Annotated[Optional[str], Cookie(alias="__Host-vignette_sid")] = None,
) -> Principal:
"""세션 쿠키 -> Principal.
TODO(auth.py 완성 시): Redis 세션 조회로 user_id/role/cohort 복원.
현재 스텁: 쿠키 없으면 401, 있으면 LEARNER 더미(개발용).
prod 에선 session_cookie 검증 실패 시 무조건 401.
"""
if not session_cookie:
# dev 환경에선 쿠키 없어도 더미 학습자로 통과(로컬 라이브 테스트). prod 는 무조건 401.
if get_settings().environment != "dev":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="not authenticated",
)
# TODO: Redis 세션 룩업. 아래는 개발 스텁.
return Principal(user_id="dev-user", role=Role.LEARNER, cohort_ids=[])
def require_role(*allowed: Role):
"""역할 화이트리스트 의존성 팩토리. 예: Depends(require_role(Role.TEACHER, Role.ADMIN))."""
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]:
"""인간 요청용 RLS 컨텍스트 커넥션 (레이어2 강제).
app.current_role 주입 -> RLS 정책이 코호트/소유권 필터.
라우트에서: conn: Annotated[asyncpg.Connection, Depends(db_for_human)]
"""
async with acquire(role=principal.role.value) as conn:
# cohort 스코프는 RLS 정책이 current_role + 소유 테이블로 강제 (설계서 §4).
yield conn
def db_for_ai_view(view: AIView):
"""AI 역할용 RLS 컨텍스트 (레이어1 강제) 의존성 팩토리.
app.current_ai_view 주입 -> visible_to[] WHERE 강제.
CLIENT 분기는 ccd/정답 로드 함수 자체를 부르지 않음(코드경로 부재 1차방어).
"""
async def _provider() -> AsyncIterator[asyncpg.Connection]:
async with acquire(ai_view=view.value) as conn:
yield conn
return _provider
# 타입 별칭 (라우트 시그니처 간결화)
CurrentPrincipal = Annotated[Principal, Depends(get_current_principal)]
HumanDB = Annotated[asyncpg.Connection, Depends(db_for_human)]