Stabilize runtime auth and E2E coverage
This commit is contained in:
parent
6a3e3b541c
commit
188e899394
133 changed files with 55987 additions and 6775 deletions
|
|
@ -1,12 +1,4 @@
|
|||
"""의존성 — 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 세션 구현 시 완성(현재 스텁).
|
||||
"""
|
||||
"""FastAPI dependencies for authentication, RBAC, and RLS context."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -16,36 +8,39 @@ 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
|
||||
|
||||
|
||||
# ── 인간 역할 (RBAC) ────────────────────────────────────
|
||||
class Role(str, Enum):
|
||||
LEARNER = "learner" # 본인 세션만 (/learn)
|
||||
TEACHER = "teacher" # 담당 코호트 전체 열람+검수 (/teach)
|
||||
ADMIN = "admin" # 전부 + 교수활동 감사 (/admin)
|
||||
LEARNER = "learner"
|
||||
TEACHER = "teacher"
|
||||
ADMIN = "admin"
|
||||
|
||||
|
||||
# ── AI 뷰 (정보비대칭, current_ai_view enum) ────────────
|
||||
class AIView(str, Enum):
|
||||
CLIENT = "client" # 가상내담자 AI — CCD/정답/점수 절대 비노출
|
||||
COUNSELOR = "counselor" # 상담사 AI(보조) — 표면 대화만, DSM 차단
|
||||
EVALUATOR = "evaluator" # 평가 AI — 전부 봄 (학습자엔 비노출)
|
||||
CLIENT = "client"
|
||||
COUNSELOR = "counselor"
|
||||
EVALUATOR = "evaluator"
|
||||
|
||||
|
||||
class Principal:
|
||||
"""인증된 요청 주체. 인간 role + (선택) cohort 범위."""
|
||||
"""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:
|
||||
|
|
@ -53,28 +48,37 @@ def get_settings_dep() -> Settings:
|
|||
|
||||
|
||||
async def get_current_principal(
|
||||
# __Host- HttpOnly 쿠키 (config.cookie_name). 브라우저엔 토큰 미노출.
|
||||
session_cookie: Annotated[Optional[str], Cookie(alias="__Host-vignette_sid")] = None,
|
||||
dev_session_cookie: Annotated[Optional[str], Cookie(alias="vignette_sid")] = None,
|
||||
) -> Principal:
|
||||
"""세션 쿠키 -> 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",
|
||||
)
|
||||
|
||||
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=[])
|
||||
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):
|
||||
"""역할 화이트리스트 의존성 팩토리. 예: Depends(require_role(Role.TEACHER, Role.ADMIN))."""
|
||||
"""Role allowlist dependency factory."""
|
||||
|
||||
async def _checker(
|
||||
principal: Annotated[Principal, Depends(get_current_principal)],
|
||||
|
|
@ -92,30 +96,24 @@ def require_role(*allowed: Role):
|
|||
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).
|
||||
"""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):
|
||||
"""AI 역할용 RLS 컨텍스트 (레이어1 강제) 의존성 팩토리.
|
||||
|
||||
app.current_ai_view 주입 -> visible_to[] WHERE 강제.
|
||||
CLIENT 분기는 ccd/정답 로드 함수 자체를 부르지 않음(코드경로 부재 1차방어).
|
||||
"""
|
||||
"""Dependency factory for AI-side RLS visibility context."""
|
||||
|
||||
async def _provider() -> AsyncIterator[asyncpg.Connection]:
|
||||
async with acquire(ai_view=view.value) as conn:
|
||||
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)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue