teecup/app/db.py

86 lines
2.7 KiB
Python
Raw Normal View History

2026-07-16 07:26:04 +02:00
"""
Databaselag for TeeCup.
Det kritiske her er hvordan organisasjonskonteksten (`app.current_org`) settes:
* Den settes med set_config(..., is_local => true), dvs. SET LOCAL verdien
gjelder KUN inne i den aktive transaksjonen og nullstilles automatisk når
transaksjonen avsluttes.
* Uten dette (en vanlig SET en pool-tilkobling) ville konteksten blitt
liggende tilkoblingen når den returneres til poolen, og neste forespørsel
potensielt en ANNEN organisasjon ville arvet den. Det ville brutt
isolasjonen ved gjenbruk av tilkoblinger.
Hver forespørsel som rører organisasjonsdata kjører altså i én transaksjon med
org-konteksten satt lokalt.
"""
from contextlib import asynccontextmanager
from typing import AsyncIterator
import asyncpg
from .config import settings
_pool: asyncpg.Pool | None = None
async def init_pool() -> None:
global _pool
_pool = await asyncpg.create_pool(
2026-07-17 21:40:42 +02:00
host=settings.DB_HOST,
port=settings.DB_PORT,
user=settings.DB_USER,
password=settings.DB_PASS,
database=settings.DB_NAME,
2026-07-16 07:26:04 +02:00
min_size=settings.DB_POOL_MIN,
max_size=settings.DB_POOL_MAX,
)
async def close_pool() -> None:
global _pool
if _pool is not None:
await _pool.close()
_pool = None
def _require_pool() -> asyncpg.Pool:
if _pool is None:
raise RuntimeError("DB-poolen er ikke initialisert (kall init_pool ved oppstart).")
return _pool
@asynccontextmanager
async def plain_connection() -> AsyncIterator[asyncpg.Connection]:
"""Tilkobling UTEN org-kontekst.
Brukes kun til auth-laget (f.eks. oppslag i organization_membership, som
ikke er organisasjonsavgrenset). Rør aldri organisasjonsdata her.
"""
pool = _require_pool()
async with pool.acquire() as conn:
yield conn
@asynccontextmanager
async def org_connection(organization_id: str) -> AsyncIterator[asyncpg.Connection]:
"""Tilkobling med org-kontekst satt lokalt for én transaksjon.
All lesing/skriving av organisasjonsdata skal gjennom denne. RLS filtrerer
da automatisk organization_id = app.current_org.
VIKTIG: `organization_id` forhånd være verifisert mot brukerens
medlemskap (se auth.get_authorized_org). Denne funksjonen stoler at
verdien allerede er autorisert.
"""
pool = _require_pool()
async with pool.acquire() as conn:
async with conn.transaction():
# SET LOCAL via set_config(..., true): nullstilles ved transaksjonsslutt.
await conn.execute(
"SELECT set_config('app.current_org', $1, true)",
str(organization_id),
)
yield conn