teecup/app/auth.py
2026-07-16 07:26:04 +02:00

71 lines
2.3 KiB
Python

"""
Auth-lag for TeeCup.
Selve innloggingen er en STUB her (erstattes med ekte sesjon/JWT senere). Det som
IKKE er en stub, og som er sikkerhetskritisk, er `get_authorized_org`: den
verifiserer at den innloggede brukeren faktisk er medlem av organisasjonen før
org-konteksten settes.
Hvorfor det er kritisk: RLS stoler blindt på `app.current_org`. Setter appen den
til en organisasjon brukeren ikke tilhører, gir RLS lydig tilgang til den
organisasjonens data. Isolasjonen står og faller altså på at denne sjekken skjer
FØR org_connection kalles — aldri sett konteksten fra en uverifisert kilde (som
en klient-oppgitt header).
"""
from dataclasses import dataclass
from fastapi import Depends, Header, HTTPException, status
from .db import plain_connection
@dataclass(frozen=True)
class CurrentUser:
user_id: str
async def get_current_user(
x_debug_user_id: str | None = Header(default=None),
) -> CurrentUser:
"""STUB: hent brukeren fra en verifisert sesjon.
Midlertidig leses bruker-id fra en header for at skjelettet skal kunne kjøres.
ERSTATT dette med validering av sesjonscookie/JWT (signert med
settings.SESSION_SECRET) før noe settes i produksjon. En header er IKKE en
autentisert kilde.
"""
if not x_debug_user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Ikke autentisert (stub forventer X-Debug-User-Id).",
)
return CurrentUser(user_id=x_debug_user_id)
async def get_authorized_org(
organization_id: str,
user: CurrentUser = Depends(get_current_user),
) -> str:
"""Returnerer organization_id KUN hvis brukeren er medlem — ellers 403.
Medlemskapstabellen er ikke organisasjonsavgrenset, så oppslaget gjøres på en
tilkobling uten org-kontekst.
"""
async with plain_connection() as conn:
is_member = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM organization_membership
WHERE user_id = $1 AND organization_id = $2
)
""",
user.user_id,
organization_id,
)
if not is_member:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Brukeren er ikke medlem av denne organisasjonen.",
)
return organization_id