RLS-tomstreng-buggen er fikset og verifisert grundig.
Fiksen: ny migrasjon 005_rls_null_guard.sql — én delt STABLE SQL-funksjon app_current_org() (NULLIF(current_setting('app.current_org', true), '')::uuid) erstatter det rå uttrykket i alle 15 RLS-policyer (14 org_isolation + org_self) via ALTER POLICY. NULLIF konverterer tomstreng til NULL før cast, så "trygg standard: se ingenting" gjenopprettes uansett GUC-tilstand.
Verifisert to ganger, ulikt:
Tre nye regresjonstester i test_isolation.sql (Test 10–12): tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, og org-bootstrap-innsetting (tidligere aldri testet) fungerer rett etter tomstreng-tilstand.
Faktisk gjenskaping av original-buggen mot en ekte container (pool-størrelse tvunget til 1 for å garantere tilkoblings-gjenbruk): varmet opp med et vanlig org_connection()-kall, kalte deretter /auth/me på samme tilkobling — gikk fra 500 til 200.
En viktig ting jeg tok feil om i forrige runde, oppdaget ved å faktisk teste i stedet for å anta: fiksen gjør ikke at /auth/me trygt kan joine organization direkte via plain_connection(). Den løser tomstreng-krasjen, men org_self-policyen krever fortsatt en matchende app.current_org for å vise noen rad i det hele tatt — riktig RLS-design, ikke noe fiksen skulle endre. Siden en bruker kan tilhøre flere organisasjoner, finnes det ingen én kontekst å sette. Rettet til riktig løsning: /auth/me slår nå opp hvert org-navn enkeltvis via org_connection() (verifisert at det faktisk returnerer navnet korrekt).
Alt dokumentert i CLAUDE.md/FEATURE_BACKLOG.md.
This commit is contained in:
parent
fbd3f58a1c
commit
9a42a2e585
6 changed files with 198 additions and 67 deletions
|
|
@ -95,7 +95,13 @@
|
||||||
"Bash(curl -s -o /dev/null -X POST -H 'Content-Type: application/json' -d '{\"email\":\"cooldown@test.no\"}' http://127.0.0.1:8099/auth/request-link)",
|
"Bash(curl -s -o /dev/null -X POST -H 'Content-Type: application/json' -d '{\"email\":\"cooldown@test.no\"}' http://127.0.0.1:8099/auth/request-link)",
|
||||||
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'Content-Type: application/json' -d '{\"email\":\"ikke-en-epost\"}' http://127.0.0.1:8099/auth/request-link)",
|
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'Content-Type: application/json' -d '{\"email\":\"ikke-en-epost\"}' http://127.0.0.1:8099/auth/request-link)",
|
||||||
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -H 'X-Debug-User-Id: 43b4ed80-5bc1-49bf-b6eb-4190f68554a2' http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/tournaments)",
|
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -H 'X-Debug-User-Id: 43b4ed80-5bc1-49bf-b6eb-4190f68554a2' http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/tournaments)",
|
||||||
"Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql __TRACKED_VAR__/cookies.txt __TRACKED_VAR__/cookies2.txt)"
|
"Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql __TRACKED_VAR__/cookies.txt __TRACKED_VAR__/cookies2.txt)",
|
||||||
|
"Bash(python3 -m py_compile /opt/teecup/app/routers/auth.py)",
|
||||||
|
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -b /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_rls.txt -X POST -H 'Content-Type: application/json' -d '{\"name\":\"Bug-repro-cup\"}' http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/tournaments)",
|
||||||
|
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -b /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_rls.txt http://127.0.0.1:8099/auth/me)",
|
||||||
|
"Bash(curl -s -o /dev/null -X POST -H 'Content-Type: application/json' -b /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_rls.txt -d '{\"name\":\"Bug-repro-cup-2\"}' http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/tournaments)",
|
||||||
|
"Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -b /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_rls.txt http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/tournaments)",
|
||||||
|
"Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql __TRACKED_VAR__/cookies_rls.txt)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
005_rls_null_guard.sql
Normal file
60
005_rls_null_guard.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
-- =====================================================================
|
||||||
|
-- TeeCup — migrasjon 005
|
||||||
|
-- RLS-fiks: tomstreng i app.current_org (se FEATURE_BACKLOG.md)
|
||||||
|
-- =====================================================================
|
||||||
|
-- Kjøres etter 001-004.
|
||||||
|
--
|
||||||
|
-- BUG: current_setting('app.current_org', true)::uuid håndterer NULL trygt
|
||||||
|
-- (gir ingen rader, som skjemaets kommentar i 001 lover: "trygg standard"),
|
||||||
|
-- men IKKE tomstreng. En gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE
|
||||||
|
-- forespørsel satte GUC-en via SET LOCAL (org_connection(), app/db.py) kan
|
||||||
|
-- lese den tilbake som '' etter at den transaksjonen er ferdig — en custom
|
||||||
|
-- GUC sin nullstilte tilstand er tomstreng når placeholderen først er
|
||||||
|
-- opprettet i sesjonen, ikke fullstendig fraværende. ''::uuid kaster en
|
||||||
|
-- feil (500) i stedet for trygt null rader. Oppdaget da /auth/me (egen
|
||||||
|
-- runde) spurte `organization` via plain_connection() (ingen org-kontekst).
|
||||||
|
--
|
||||||
|
-- FIKS: NULLIF(current_setting(...), '')::uuid — konverterer tomstreng til
|
||||||
|
-- NULL FØR cast. Samlet i én STABLE SQL-funksjon fremfor duplisert i 15
|
||||||
|
-- policyer.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
-- Feil raskt fremfor å blokkere hele appen hvis en ALTER POLICY skulle køe
|
||||||
|
-- bak en lang spørring — hver ALTER POLICY under tar ACCESS EXCLUSIVE på
|
||||||
|
-- måltabellen til COMMIT.
|
||||||
|
SET lock_timeout = '5s';
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION app_current_org() RETURNS uuid
|
||||||
|
LANGUAGE sql STABLE AS $$
|
||||||
|
SELECT NULLIF(current_setting('app.current_org', true), '')::uuid
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- De 14 org_isolation-tabellene: 12 fra 001 + 2 fra 003 (samme array som
|
||||||
|
-- CREATE-loopene der — hold denne i sync hvis en fremtidig migrasjon
|
||||||
|
-- legger til en ny org-scopet tabell).
|
||||||
|
DO $$
|
||||||
|
DECLARE t text;
|
||||||
|
BEGIN
|
||||||
|
FOREACH t IN ARRAY ARRAY[
|
||||||
|
'player','course','hole','tee','tee_rating',
|
||||||
|
'tournament','team','team_roster','session',
|
||||||
|
'match','match_participant','hole_score',
|
||||||
|
'match_hole_result','lineup_lock'
|
||||||
|
]
|
||||||
|
LOOP
|
||||||
|
EXECUTE format($p$
|
||||||
|
ALTER POLICY org_isolation ON %I
|
||||||
|
USING (organization_id = app_current_org())
|
||||||
|
WITH CHECK (organization_id = app_current_org());
|
||||||
|
$p$, t);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- org_self ble opprinnelig laget UTEN eksplisitt WITH CHECK (polwithcheck er
|
||||||
|
-- NULL i pg_policy, ikke en snapshot av USING) — Postgres sin
|
||||||
|
-- speiler-USING-når-WITH-CHECK-mangler-fallback er dynamisk, ikke en
|
||||||
|
-- engangskopi tatt ved CREATE. Å re-issue kun USING her er derfor korrekt.
|
||||||
|
ALTER POLICY org_self ON organization
|
||||||
|
USING (id = app_current_org());
|
||||||
39
CLAUDE.md
39
CLAUDE.md
|
|
@ -102,23 +102,30 @@ Ferdig og verifisert:
|
||||||
**Fant og fikset underveis:** `ON CONFLICT (email)` matchet ikke den nye
|
**Fant og fikset underveis:** `ON CONFLICT (email)` matchet ikke den nye
|
||||||
PARTIELLE unike indeksen uten eksplisitt `WHERE email IS NOT NULL` (samme
|
PARTIELLE unike indeksen uten eksplisitt `WHERE email IS NOT NULL` (samme
|
||||||
klasse feil som `hole_score`s partielle indekser i scoring-runden).
|
klasse feil som `hole_score`s partielle indekser i scoring-runden).
|
||||||
**Fant, IKKE fikset her (egen sak, se FEATURE_BACKLOG):** `organization`-
|
**Fant, IKKE fikset i denne runden (egen runde rett etterpå — se under):**
|
||||||
tabellens RLS-policy (`org_self`, og trolig ALLE `org_isolation`-policyer i
|
`organization`-tabellens RLS-policy kastet en 500 i stedet for skjemaets
|
||||||
001/003) kaster en 500 i stedet for skjemaets lovede "trygg standard: se
|
lovede "trygg standard: se ingenting" ved tomstreng-GUC.
|
||||||
ingenting" når `current_setting('app.current_org', true)` returnerer
|
- **RLS-tomstreng-bug FIKSET (2026-07-16):** ny migrasjon
|
||||||
TOMSTRENG (ikke NULL) — noe som kan skje på en gjenbrukt asyncpg-pool-
|
`005_rls_null_guard.sql` — delt `STABLE` SQL-funksjon `app_current_org()`
|
||||||
tilkobling der en tidligere forespørsel satte GUC-en via `SET LOCAL`. Kun
|
gjør `NULLIF(current_setting('app.current_org', true), '')::uuid` i stedet
|
||||||
et problem for kode som spør org-scopede tabeller via `plain_connection()`
|
for det rå uttrykket, brukt av alle 15 RLS-policyer (`ALTER POLICY`,
|
||||||
(ingen org-kontekst) — `/auth/me` unngår det bevisst ved å ikke joine mot
|
14 `org_isolation` + `org_self`). Verifisert med 3 nye regresjonstester i
|
||||||
`organization`. Fiksen (`NULLIF(current_setting(...), '')::uuid` i alle 15
|
`test_isolation.sql` (Test 10-12) OG ved faktisk å gjenskape original-
|
||||||
policyer) er reell, billig, og lav risiko, men berører selve
|
buggen mot en ekte container (pool-størrelse 1, varm opp med
|
||||||
isolasjonsgrunnmuren (ADR-003) og fortjener en egen, fokusert
|
`org_connection()`, deretter `/auth/me` på samme gjenbrukte tilkobling —
|
||||||
rettingsrunde med skikkelig testing — ikke en hastefiks boltet på noe annet.
|
gikk fra 500 til 200).
|
||||||
|
**Viktig presisering fra denne runden:** fiksen gjør IKKE at `/auth/me` kan
|
||||||
|
joine `organization` direkte via `plain_connection()` — det var en feilaktig
|
||||||
|
antakelse i forrige runde. `org_self` krever fortsatt en MATCHENDE
|
||||||
|
`app.current_org` for å vise en rad (riktig RLS-design, ikke noe fiksen
|
||||||
|
skulle endre), og en bruker kan tilhøre flere organisasjoner samtidig, så
|
||||||
|
det finnes ingen ÉN kontekst å sette for en tverr-org-spørring. `/auth/me`
|
||||||
|
slår derfor opp hvert org-navn ett om gangen via `org_connection()` (N+1,
|
||||||
|
N = antall org-er brukeren tilhører) — dette er riktig løsning, ikke en
|
||||||
|
omvei.
|
||||||
|
|
||||||
Neste steg:
|
Neste steg:
|
||||||
1. **Fiks RLS-tomstreng-buggen** beskrevet over (egen liten runde, migrasjon
|
1. Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot
|
||||||
005) — reell, men isolert og lavrisiko.
|
|
||||||
2. Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot
|
|
||||||
`teecup_db` med `teecup_app`, rute via eksisterende Caddy til
|
`teecup_db` med `teecup_app`, rute via eksisterende Caddy til
|
||||||
`teecup.teeoff.no`. (Under scratch-verifisering måtte hele `/opt/teecup`
|
`teecup.teeoff.no`. (Under scratch-verifisering måtte hele `/opt/teecup`
|
||||||
monteres, ikke bare `app/`, fordi `handicap_engine.py` er et
|
monteres, ikke bare `app/`, fordi `handicap_engine.py` er et
|
||||||
|
|
@ -126,5 +133,5 @@ Neste steg:
|
||||||
med samme relative plassering.) Ekte SMTP-utsending av magic-link må også
|
med samme relative plassering.) Ekte SMTP-utsending av magic-link må også
|
||||||
kobles inn før dette går live (i dag: dev-only logging bak
|
kobles inn før dette går live (i dag: dev-only logging bak
|
||||||
`TEECUP_DEV_LOG_MAGIC_LINKS`).
|
`TEECUP_DEV_LOG_MAGIC_LINKS`).
|
||||||
3. Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 006,
|
2. Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 006,
|
||||||
siden 004/005 nå er tatt av auth og RLS-fiksen).
|
siden 004/005 nå er tatt av auth og RLS-fiksen).
|
||||||
|
|
|
||||||
|
|
@ -28,34 +28,38 @@
|
||||||
| Banedata fra teeoff via API | 🔀 | ADR-004. Endret fra Geminis «delt database direkte». |
|
| Banedata fra teeoff via API | 🔀 | ADR-004. Endret fra Geminis «delt database direkte». |
|
||||||
| Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. |
|
| Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. |
|
||||||
| Ekte autentisering (magic-link + JWT-sesjon) | ✅ | ADR-009. `app/routers/auth.py` + migrasjon `004_auth.sql`. `X-Debug-User-Id`-stubben er helt fjernet. Ekte SMTP-utsending gjenstår (i dag: dev-only logging). |
|
| Ekte autentisering (magic-link + JWT-sesjon) | ✅ | ADR-009. `app/routers/auth.py` + migrasjon `004_auth.sql`. `X-Debug-User-Id`-stubben er helt fjernet. Ekte SMTP-utsending gjenstår (i dag: dev-only logging). |
|
||||||
|
| RLS-tomstreng-fiks (`app_current_org()`) | ✅ | Migrasjon `005_rls_null_guard.sql`. Se detaljer under. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### RLS-tomstreng-bug (funnet 2026-07-16, IKKE fikset ennå)
|
### RLS-tomstreng-bug — ✅ FIKSET 2026-07-16
|
||||||
- **Status:** ❓ trenger egen rettingsrunde (lavrisiko, men berører ADR-003s
|
- Alle RLS-policyer i 001/003 (`org_isolation` på 14 tabeller + `org_self` på
|
||||||
isolasjonsgrunnmur — fortjener fokusert testing, ikke en hastefiks).
|
`organization`) brukte `current_setting('app.current_org', true)::uuid`.
|
||||||
- Alle RLS-policyer i 001/003 (`org_isolation` på 13 tabeller + `org_self` på
|
Denne håndterte NULL trygt (ga ingen rader, som tiltenkt), men IKKE
|
||||||
`organization`) bruker `current_setting('app.current_org', true)::uuid`.
|
tomstreng — en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE
|
||||||
Denne håndterer NULL trygt (gir ingen rader, som tiltenkt), men IKKE
|
forespørsel satte GUC-en via `SET LOCAL` kunne lese den tilbake som `''`
|
||||||
tomstreng — og en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE
|
etter at transaksjonen var ferdig, og casten kastet da en 500 i stedet for
|
||||||
forespørsel satte GUC-en via `SET LOCAL` kan lese den tilbake som `''`
|
skjemaets lovede "trygg standard: se ingenting".
|
||||||
(tomstreng) i stedet for NULL etter at den transaksjonen er ferdig. Da
|
- **Fiks:** samlet i én `STABLE` SQL-funksjon `app_current_org()` (migrasjon
|
||||||
kaster casten en 500 (`invalid input syntax for type uuid: ""`) i stedet
|
`005_rls_null_guard.sql`) som gjør `NULLIF(current_setting(...), '')::uuid`
|
||||||
for skjemaets lovede "trygg standard: se ingenting".
|
— konverterer tomstreng til NULL FØR cast. Alle 15 policyer alteret
|
||||||
- **Oppdaget av:** `/auth/me` (ny denne runden) prøvde å joine mot
|
(`ALTER POLICY`) til å bruke funksjonen i stedet for det rå uttrykket.
|
||||||
`organization`-tabellen via `plain_connection()` (ingen org-kontekst) for å
|
Verifisert med 3 nye regresjonstester i `test_isolation.sql` (Test 10-12:
|
||||||
hente organisasjonsnavn til en multi-org-liste — det er FØRSTE gang noe
|
tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS
|
||||||
spør en org-scopet, RLS-beskyttet tabell via en tilkobling uten
|
ikke krasj, org-bootstrap-innsetting fungerer rett etter tomstreng-
|
||||||
org-kontekst. Mitigert MIDLERTIDIG i `/auth/me` ved rett og slett å ikke
|
tilstand) OG ved å faktisk gjenskape original-buggen mot en ekte container
|
||||||
joine mot `organization` (returnerer kun `organization_id` + `role`, ikke
|
(pool-størrelse 1, varm opp med `org_connection()`, deretter kall
|
||||||
navn) — unngår buggen, løser den ikke.
|
`/auth/me` på samme gjenbrukte tilkobling — gikk fra 500 til 200).
|
||||||
- **Fiks:** `NULLIF(current_setting('app.current_org', true), '')::uuid` i
|
- **Viktig presisering oppdaget underveis:** den opprinnelige planen antok at
|
||||||
stedet for `current_setting(...)::uuid`, i alle 15 policyer (`ALTER
|
`/auth/me` kunne joine `organization` direkte igjen når tomstreng-buggen
|
||||||
POLICY`, egen migrasjon 005). NULLIF konverterer tomstreng til NULL FØR
|
var fikset. Det var FEIL — fiksen gjør bare at tomstreng oppfører seg som
|
||||||
cast, så den trygge "se ingenting"-oppførselen gjenopprettes uansett hvilken
|
NULL (trygt: se ingenting), den endrer IKKE at `org_self`-policyen krever
|
||||||
tilstand GUC-en er i.
|
en MATCHENDE `app.current_org` for å vise en rad i det hele tatt (riktig
|
||||||
- **Følgeoppgave når fikset:** `/auth/me` kan da trygt joine mot
|
RLS-oppførsel, ikke en bug). En bruker kan tilhøre flere organisasjoner
|
||||||
`organization` igjen og returnere organisasjonsnavn, ikke bare ID+rolle.
|
samtidig, så det finnes ingen ÉN kontekst å sette for en tverr-org-
|
||||||
|
spørring. `/auth/me` slår derfor opp hvert org-navn ETT OM GANGEN via
|
||||||
|
`org_connection()` (N+1 spørringer, N = antall org-er brukeren tilhører,
|
||||||
|
typisk 1-3) — verifisert at dette faktisk returnerer navnet korrekt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from pydantic import BaseModel, EmailStr
|
||||||
|
|
||||||
from ..auth import CurrentUser, SESSION_COOKIE_NAME, create_session_token, get_current_user, should_use_secure_cookies
|
from ..auth import CurrentUser, SESSION_COOKIE_NAME, create_session_token, get_current_user, should_use_secure_cookies
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..db import plain_connection
|
from ..db import org_connection, plain_connection
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
@ -156,6 +156,7 @@ async def logout(response: Response) -> dict:
|
||||||
|
|
||||||
class MyOrg(BaseModel):
|
class MyOrg(BaseModel):
|
||||||
organization_id: str
|
organization_id: str
|
||||||
|
name: str
|
||||||
role: str
|
role: str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,33 +169,37 @@ class Me(BaseModel):
|
||||||
|
|
||||||
@router.get("/me", response_model=Me)
|
@router.get("/me", response_model=Me)
|
||||||
async def me(user: CurrentUser = Depends(get_current_user)) -> Me:
|
async def me(user: CurrentUser = Depends(get_current_user)) -> Me:
|
||||||
# MERK: henter bevisst IKKE organisasjonens navn her. Det ville krevd å
|
# MERK: `organization` (org_self-policyen) kan IKKE joines direkte her via
|
||||||
# spørre `organization`-tabellen (som HAR RLS, org_self-policyen) via
|
# plain_connection() -- migrasjon 005 fikset kun tomstreng-krasjen, den
|
||||||
# plain_connection() -- altså UTEN org-kontekst satt. Under scratch-
|
# endret ikke at org_self krever en MATCHENDE app.current_org for å vise
|
||||||
# testing avdekket dette en reell, dypere bug: org_self sin
|
# en rad i det hele tatt (riktig RLS-oppførsel, ikke en bug). En bruker
|
||||||
# `current_setting('app.current_org', true)::uuid` håndterer NULL trygt,
|
# kan tilhøre flere organisasjoner samtidig, så det finnes ingen ÉN
|
||||||
# men IKKE tomstreng (som en custom GUC kan lese tilbake som på en
|
# kontekst å sette for en tverr-org-spørring som denne. Løsningen er
|
||||||
# gjenbrukt pool-tilkobling der en annen forespørsel tidligere satte den
|
# derfor å slå opp hvert org-navn ETT OM GANGEN gjennom org_connection()
|
||||||
# via SET LOCAL) -- det gir en 500 i stedet for skjemaets lovede "trygg
|
# (som setter riktig kontekst for akkurat den ene raden) -- N+1 spørringer,
|
||||||
# standard: se ingenting". Dette er en tverrgående RLS-sak (berører alle
|
# men N er antall organisasjoner brukeren tilhører (typisk 1-3), og dette
|
||||||
# 15 policyer i skjemaet, ikke noe auth-spesifikt) som fortjener sin egen
|
# er den eneste måten å gjøre det på uten å endre selve RLS-modellen.
|
||||||
# fokuserte rettingsrunde, ikke en hastefiks her. Se FEATURE_BACKLOG.md.
|
|
||||||
async with plain_connection() as conn:
|
async with plain_connection() as conn:
|
||||||
user_row = await conn.fetchrow(
|
user_row = await conn.fetchrow(
|
||||||
"SELECT id::text AS id, email::text AS email, display_name FROM app_user WHERE id = $1",
|
"SELECT id::text AS id, email::text AS email, display_name FROM app_user WHERE id = $1",
|
||||||
user.user_id,
|
user.user_id,
|
||||||
)
|
)
|
||||||
org_rows = await conn.fetch(
|
membership_rows = await conn.fetch(
|
||||||
"""
|
"SELECT organization_id::text AS organization_id, role FROM organization_membership WHERE user_id = $1",
|
||||||
SELECT organization_id::text AS organization_id, role
|
|
||||||
FROM organization_membership
|
|
||||||
WHERE user_id = $1
|
|
||||||
""",
|
|
||||||
user.user_id,
|
user.user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
organizations = []
|
||||||
|
for m in membership_rows:
|
||||||
|
async with org_connection(m["organization_id"]) as org_conn:
|
||||||
|
name = await org_conn.fetchval(
|
||||||
|
"SELECT name FROM organization WHERE id = $1", m["organization_id"]
|
||||||
|
)
|
||||||
|
organizations.append(MyOrg(organization_id=m["organization_id"], name=name, role=m["role"]))
|
||||||
|
|
||||||
return Me(
|
return Me(
|
||||||
id=user_row["id"],
|
id=user_row["id"],
|
||||||
email=user_row["email"],
|
email=user_row["email"],
|
||||||
display_name=user_row["display_name"],
|
display_name=user_row["display_name"],
|
||||||
organizations=[MyOrg(**dict(r)) for r in org_rows],
|
organizations=organizations,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@
|
||||||
-- sett fra runtime-rollen teecup_app (som IKKE er superuser/BYPASSRLS).
|
-- sett fra runtime-rollen teecup_app (som IKKE er superuser/BYPASSRLS).
|
||||||
--
|
--
|
||||||
-- Kjør som admin/superbruker mot en scratch- eller test-database der
|
-- Kjør som admin/superbruker mot en scratch- eller test-database der
|
||||||
-- 001_initial_schema.sql, 002_roles_and_grants.sql og
|
-- 001_initial_schema.sql, 002_roles_and_grants.sql,
|
||||||
-- 003_scoring_and_blinddraw.sql allerede er kjørt (003 kreves for
|
-- 003_scoring_and_blinddraw.sql og 005_rls_null_guard.sql allerede er kjørt
|
||||||
-- match_hole_result/lineup_lock, testet her i tillegg til grunnskjemaet):
|
-- (003 kreves for match_hole_result/lineup_lock, 005 for
|
||||||
|
-- app_current_org()-funksjonen testene 10-12 bruker indirekte):
|
||||||
-- psql -d teecup_scratch -f test_isolation.sql
|
-- psql -d teecup_scratch -f test_isolation.sql
|
||||||
--
|
--
|
||||||
-- Alt kjøres i én transaksjon som RULLES TILBAKE til slutt — ingen testdata
|
-- Alt kjøres i én transaksjon som RULLES TILBAKE til slutt — ingen testdata
|
||||||
|
|
@ -202,13 +203,61 @@ BEGIN
|
||||||
RAISE NOTICE 'OK Test 9b: lineup_lock — kontekstbytte gir riktig organisasjons data (%).', tid;
|
RAISE NOTICE 'OK Test 9b: lineup_lock — kontekstbytte gir riktig organisasjons data (%).', tid;
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
-- --- Regresjonstester for RLS-tomstreng-buggen (migrasjon 005) ----------
|
||||||
|
-- current_setting('app.current_org', true)::uuid håndterte NULL trygt, men
|
||||||
|
-- IKKE tomstreng — en gjenbrukt pool-tilkobling kunne lese GUC-en tilbake
|
||||||
|
-- som '' og få en kastet feil i stedet for trygt "se ingenting". Simulerer
|
||||||
|
-- tilstanden direkte (set_config med is_local=false, ikke SET LOCAL) siden
|
||||||
|
-- den er lettere å fremtvinge deterministisk enn selve pool-gjenbruken som
|
||||||
|
-- utløste den i praksis.
|
||||||
|
|
||||||
|
-- Test 10: tomstreng-GUC — SELECT skal gi 0 rader, IKKE en kastet feil.
|
||||||
|
SELECT set_config('app.current_org', '', false);
|
||||||
|
DO $$
|
||||||
|
DECLARE n int;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO n FROM tournament;
|
||||||
|
IF n <> 0 THEN
|
||||||
|
RAISE EXCEPTION 'ISOLASJON FEILET (tomstreng-lesing): ser % rader, forventet 0', n;
|
||||||
|
END IF;
|
||||||
|
RAISE NOTICE 'OK Test 10: tomstreng-GUC — SELECT gir trygt 0 rader, ikke feil (%).', n;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Test 11: tomstreng-GUC — INSERT skal avvises av RLS (insufficient_privilege),
|
||||||
|
-- IKKE feile med "invalid input syntax for type uuid".
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO tournament (organization_id, name)
|
||||||
|
VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'tomstreng-forsøk');
|
||||||
|
RAISE EXCEPTION 'ISOLASJON FEILET (tomstreng-skriving): fikk sette inn data uten gyldig org-kontekst';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN insufficient_privilege THEN
|
||||||
|
RAISE NOTICE 'OK Test 11: tomstreng-GUC — WITH CHECK blokkerte innsetting trygt (ikke krasj).';
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Test 12: bootstrap-mønster (sett current_org til en FERSK uuid, deretter
|
||||||
|
-- INSERT organization med SAMME id) skal fungere korrekt selv RETT ETTER at
|
||||||
|
-- tilstanden nettopp var tomstreng — beviser at en eksplisitt satt verdi
|
||||||
|
-- alltid overstyrer uansett tidligere GUC-tilstand. Dobler som første test
|
||||||
|
-- av selve org-bootstrap-innsettingsstien (002s kommentar, tidligere utestet).
|
||||||
|
SELECT set_config('app.current_org', 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', false);
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO organization (id, name)
|
||||||
|
VALUES ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'Bootstrap-test-org');
|
||||||
|
RAISE NOTICE 'OK Test 12: org-bootstrap — INSERT lykkes rett etter tomstreng-tilstand.';
|
||||||
|
END $$;
|
||||||
|
|
||||||
RESET ROLE;
|
RESET ROLE;
|
||||||
RESET app.current_org;
|
RESET app.current_org;
|
||||||
|
|
||||||
ROLLBACK; -- ingen testdata blir liggende igjen
|
ROLLBACK; -- ingen testdata blir liggende igjen
|
||||||
|
|
||||||
\echo '============================================'
|
\echo '============================================'
|
||||||
\echo ' Alle 9 isolasjonstester bestått (se OK-linjer)'
|
\echo ' Alle 12 isolasjonstester bestått (se OK-linjer)'
|
||||||
\echo ' Dekker: tournament/organization (001) samt'
|
\echo ' Dekker: tournament/organization (001),'
|
||||||
\echo ' match_hole_result/lineup_lock (003, ADR-012/013)'
|
\echo ' match_hole_result/lineup_lock (003, ADR-012/013),'
|
||||||
|
\echo ' og RLS-tomstreng-fiksen (005)'
|
||||||
\echo '============================================'
|
\echo '============================================'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue