Det viktigste funnet: kommentaren i 002_roles_and_grants.sql om at bootstrap trenger "en privilegert sti" stemte ikke. Den selvrefererende triksen fungerte på første forsøk: generer org-ens uuid i Python før innsetting, sett app.current_org til nøyaktig den via den allerede eksisterende org_connection(), sett så inn raden med samme id — org_selfs implisitte WITH CHECK blir da trivielt sann. teecup_app (NOSUPERUSER/NOBYPASSRLS) trengte ingen egen privilegert tilkobling. Alle 5 tester fra planen bestått, inkludert den viktigste — en negativ kontroll som beviser mekanismen er presis og ikke et RLS-hull: forsøk på å sette inn en organisasjon med en mismatchende id ble avvist med insufficient_privilege. I tillegg: ny org fungerer normalt med eksisterende endepunkter, dukker riktig opp i /auth/me, og full kryss-org-isolasjon holder mellom to uavhengig opprettede organisasjoner. CLAUDE.md og FEATURE_BACKLOG.md er oppdatert.
61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
"""
|
|
Organisasjon-bootstrap (opprett en ny organisasjon og bli owner).
|
|
|
|
Eneste stedet i API-et som setter inn en `organization`-rad. Bruker
|
|
`get_current_user` (IKKE `get_authorized_org`, som forutsetter en ALLEREDE
|
|
eksisterende org-medlemskap -- sirkulært for akkurat denne handlingen, samme
|
|
resonnement som lag-/roster-opprettelse i oppsett-runden).
|
|
|
|
Selvrefererende RLS-bootstrap: `org_self`-policyen (etter migrasjon 005) er
|
|
`USING (id = app_current_org())` uten eksplisitt WITH CHECK, som Postgres
|
|
speiler dynamisk til INSERT-sjekken. Genererer derfor org-ens uuid FØR
|
|
innsetting og setter `app.current_org` til NØYAKTIG den verdien via den
|
|
allerede eksisterende `org_connection()` -- da er WITH CHECK trivielt sann
|
|
(id == current_org), og `teecup_app` (NOSUPERUSER/NOBYPASSRLS) kan sette inn
|
|
sin egen FØRSTE organisasjonsrad uten noen egen privilegert tilkobling.
|
|
Verifisert med en negativ kontroll (mismatchende id blir avvist) i
|
|
scratch-testingen -- se plan.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel, Field
|
|
|
|
from ..auth import CurrentUser, get_current_user
|
|
from ..db import org_connection
|
|
from ..errors import translate_db_errors
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class OrganizationCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
|
class OrganizationOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
role: str
|
|
|
|
|
|
@router.post("/orgs", response_model=OrganizationOut, status_code=201)
|
|
async def create_organization(
|
|
body: OrganizationCreate,
|
|
user: CurrentUser = Depends(get_current_user),
|
|
) -> OrganizationOut:
|
|
new_id = str(uuid.uuid4())
|
|
|
|
async with org_connection(new_id) as conn, translate_db_errors():
|
|
row = await conn.fetchrow(
|
|
"INSERT INTO organization (id, name) VALUES ($1, $2) RETURNING id::text, name",
|
|
new_id,
|
|
body.name,
|
|
)
|
|
await conn.execute(
|
|
"INSERT INTO organization_membership (organization_id, user_id, role) VALUES ($1, $2, 'owner')",
|
|
new_id,
|
|
user.user_id,
|
|
)
|
|
|
|
return OrganizationOut(id=row["id"], name=row["name"], role="owner")
|