TL;DR
Supabase row level security (zabezpečenie na úrovni riadkov) testujte tak, ako sa s ním stretne člen tímu: prihláste sa za každú rolu, spustite skutočnú akciu raz s vypnutým a raz so zapnutým oprávnením a overte, že sa výsledok zmení. Na produkčnej aplikácii s piatimi upraviteľnými rolami takto vyšlo najavo, že 30 z 87 políčok oprávnení, ktoré mohol admin uložiť, v databáze nemenilo nič. Test po politikách to nevidí, pretože testuje politiku, ktorú ste napísali, nie políčko na obrazovke.
Som Denys, full-stack a AI inžinier. Tento rok som pre audítorskú firmu postavil nástroj na sledovanie projektov a času na Supabase, kde každé pravidlo prístupu žije v databáze. Nižšie je metóda, ktorou tieto pravidlá dokazujem, čo našla a koľko stojí ju spúšťať.
Prečo testy politík nestačia na test Supabase RLS?
Test politiky odpovedá na otázku „robí táto politika to, čo som napísal?“. Vaši používatelia sa pýtajú inak: „ak tejto roli odškrtnem toto políčko, dokáže to ešte urobiť?“. V malej aplikácii majú obe otázky rovnakú odpoveď. V aplikácii so stránkou nastavení, kde admini upravujú maticu oprávnení, sa rozchádzajú.
Aplikácia mala 41 tabuliek so zapnutým RLS a 402 príkazov CREATE POLICY v 255 migráciách. Obrazovka admina ukazovala maticu oblastí (projekty, úlohy, výkazy, súbory a podobne) proti akciám (zobraziť, vytvoriť, upraviť, archivovať), jeden stĺpec na rolu. Každé uložené políčko bolo sľubom adminovi. Nič neoverovalo, že ho databáza dodrží.
Supabase štandardné nástroje dobre dokumentuje: testy pgTAP a testovacie pomocné funkcie vám dovolia vytvoriť používateľov, prepnúť sa na nich a overiť, čo môžu čítať. Používajte ich. Metóda nižšie stojí nad nimi: riadi ju matica, ktorú vidí admin, nie zoznam politík.
Ako funguje overenie matice?
Každé políčko matice sa stane sondou. Sonda sa prihlási ako člen roly, vykoná jednu skutočnú akciu a zaznamená výsledok. Urobí to dvakrát: raz s vypnutým a raz so zapnutým políčkom, všetko ostatné rovnaké. Ak sa výsledok zmení, políčko sa vynucuje. Ak nie, políčko nerobí nič, nech na obrazovke vyzerá akokoľvek presvedčivo.
- Jedno políčko v maticiPríklad: Standard Member · Úlohy · Vytvoriť
- Políčko vypnuté: konať ako člen rolyVytvoriť úlohu rovnakou cestou, akou to robí aplikácia. Zaznamenať výsledok.
- Políčko zapnuté: zopakovať tú istú akciuRovnaký používateľ, rovnaké dáta. Zmenilo sa iba políčko.
- Porovnať oba výsledkyVerdikt závisí iba od toho, čo urobila databáza.
- Výsledok sa zmenilVynucuje sa. Vypnuté odmietne, zapnuté povolí.
- V oboch prípadoch rovnakýNerobí nič. Obrazovka sľubuje kontrolu, ktorá neexistuje.
- Obrazovka a databáza sa nezhodujúChyba. Stránka ukazuje tlačidlo, ktoré databáza odmietne, alebo naopak.
„Výsledok“ tu znamená čokoľvek, čo urobí spotrebiteľ oprávnenia: politika vráti riadky alebo odmietne zápis, databázová funkcia vyhodí chybu, trigger zablokuje, alebo edge funkcia odpovie 403. Jedno políčko môže strážiť viac z nich, takže 87 políčok si vyžiadalo 279 kontrol na rolu. Každá kontrola skončila jedným z týchto verdiktov:
| Verdikt | Kontroly na rolu | Čo znamená |
|---|---|---|
| Účinok | 227 | Výsledok sa s políčkom otočil |
| Mŕtve | 30 | Výsledok zostal rovnaký s vypnutým aj zapnutým políčkom |
| Nedosiahnuteľné | 19 | Akcia je pre túto rolu vôbec nedosiahnuteľná, dokázané samostatne |
| Edge funkcia | 1 | Vynucuje edge funkcia, overené cez HTTP |
| Iba obrazovka | 1 | Mení iba to, čo stránka ukazuje, pokryté front-end testom |
| Obrazovka a databáza sa zhodujú | 1 | Čo stránka ponúka, zodpovedá tomu, čo databáza povolí |
| Zlyhanie | 0 | Kontrola, ktorej výsledok nezodpovedal očakávaniu |
Čo overenie našlo?
Beh z 15. septembra 2026 pokryl všetkých päť rolí, ktoré môže admin upravovať (Admin, Senior, Standard, Restricted a jednu naklonovanú vlastnú rolu). Výsledky boli pre všetky roly rovnaké.
Žiadne z 30 nikoho nepustilo dnu. Pri 29 z nich políčko nikdy nečítala žiadna politika, funkcia ani trigger; posledné prestalo mať význam po prepísaní jednej politiky. Aj tak je to skutočný problém: admin, ktorý roli odškrtne „Zmazať“, verí, že rola nemôže mazať. Všetkých 30 sme skryli namiesto toho, aby sme ich zapojili, pretože každé by si vyžiadalo produktové rozhodnutie a políčko, ktoré nerobí nič, nemá byť na obrazovke, kým sa rozhodujete.
Tá istá práca odhalila ďalšie štyri chyby:
| Čo bolo zle | Ako sa to prejavilo | Oprava |
|---|---|---|
| 30 políčok nerobilo nič | Verdikt Mŕtve, rovnaký výsledok vypnuté aj zapnuté | Skryté z matice |
| Tri databázové funkcie pustili akciu, keď úloha nemala riešiteľa | Sonda, ktorá mala byť odmietnutá, bola povolená | Funkcie teraz odmietnu, keď hodnota chýba |
| Vrátenie výkazu do konceptu záviselo od nesprávneho políčka | Obrazovka a databáza sa pri jednej role nezhodovali | Viazané na oprávnenie na znovuotvorenie, kam patrí |
| Člen videl nové oprávnenia až po obnovení stránky | Databáza sa zmenila, stránka nie | Oprávnenia sa obnovujú každých 60 sekúnd a pri fokuse okna |
| Predvolená hodnota stĺpca porušovala vlastné check obmedzenie, takže vloženia bez neho padali | Našla to bezpečnostná kontrola opravy, počas tej istej práce | Predvolená hodnota zmenená na takú, ktorú obmedzenie povolí |
Na chybe s chýbajúcim riešiteľom záležalo najviac. Stráže boli v PL/pgSQL napísané takto:
if not (has_permission or assignee_id = auth.uid()) then
raise exception 'forbidden';
end if;
Keď je assignee_id null, assignee_id = auth.uid() je null, false or null je null, not null je stále null a if berie null ako false. Výnimka sa nikdy nevyhodí. V produkcii nemala nastaveného riešiteľa žiadna úloha, takže nešlo o okrajový prípad: týkalo sa to každej úlohy v rozsahu. Oprava obalí podmienku do coalesce(…, false), aby chýbajúca hodnota znamenala odmietnutie, a išla von ako prvá, samostatne, pred zvyškom dávky.
Ako to spustiť proti kópii produkcie?
Dôkaz proti schéme, ktorá sa líši od produkcie, o produkcii nedokazuje nič. Harness beží na lokálnom Supabase stacku, v samostatnej databáze, ktorá je kópiou produkčnej schémy plus migrácie na kontrolu. Pred každým behom porovná odtlačok oboch lokálnych databáz: 799 riadkov opisujúcich tabuľky, politiky, funkcie a granty. Akýkoľvek rozdiel beh zastaví.
- Produkčná schémaSkopírovaná na jeden dôveryhodný stroj. Nikdy nejde na hostovaný CI runner.
- Lokálny stack: databáza aplikácieObsluhuje ju lokálne API a auth, používajú ju end-to-end kontroly.
- Lokálny stack: izolovaná databáza na overenieRovnaká schéma, používa ju iba overenie matice.
- Kontrola odtlačku799 riadkov na databázu. Akýkoľvek rozdiel zastaví beh ešte pred prvou sondou.
- Sondy bežia za každú roluAko skutoční prihlásení členovia, rovnakými cestami, aké používa aplikácia.
- Bez stopyPočty riadkov na konci sa musia rovnať počtom na začiatku.
Dôveryhodným urobili harness dva detaily:
- Najprv ho zámerne pokazte. Skôr než som uveril zelenému behu, nahradil som jednu insert politiku takou, ktorá prijme všetko, a overenie spustil znova. Muselo zčervenať na políčkach, ktoré táto politika stráži. Harness, ktorý zostane zelený aj pri pokazenej politike, je dekorácia.
- Overte aj uložený stav. Prvý prechod začína každú rolu z čistého základu. Druhý začína z aktuálne uloženej matice každej roly, pretože políčko sa môže správať inak vedľa políčok, ktoré admin naozaj zaškrtol.
Ako ho udržať zelený po vydaní?
Overenie spustené raz zastará s ďalšou migráciou. Teraz beží pred každým pushom, ktorý sa dotkne migrácií, auth kódu alebo samotnej matice.
- git pushZmena sa dotýka migrácií, auth alebo matice.
- Pre-push hook: overenie maticeBeží lokálne proti databáze na overenie. Červená push zablokuje.
- Push sa dostane do mainNasadenie beží ako obvykle.
- Beh na self-hosted CITo isté overenie znova, na stroji, kde je kópia produkčnej schémy.
Overenie tvorí 135 testov v 11 súboroch, popri zhruba 3 100 bežných unit testoch v tom istom projekte.
Čo nefungovalo?
- Hostované CI ho nevedelo spustiť. Overenie potrebuje kópiu produkčnej schémy a tú som na hostovanom runneri mať nechcel. Blokujúca brána je preto pre-push hook a CI na vlastnom stroji je druhá kontrola, na ktorú nasadenie nečaká.
- Pevné počty riadkov sa rozbili. Prvá kontrola „bez stopy“ porovnávala s pevnými číslami. Ďalšia legitímna migrácia pridala riadok oprávnenia a kontrola zčervenala. Teraz porovnáva počty na konci s počtami na začiatku.
- Preskočené nie je úspešné. End-to-end kontroly potrebujú lokálny edge runtime. Keď nebeží, preskočia sa a preskočenie sa ľahko prečíta ako zelená.
- Na vyťaženom notebooku je pomalé. Prechody po rolách potrebovali na zaťaženom stroji dlhšie timeouty.
- Dokazuje to, čo sondy opisujú. Políčko, pre ktoré nikto nenapísal sondu, sa hlási ako medzera v pokrytí, nie ako bezpečné.
Ako to zopakovať vo vašej Supabase aplikácii
- Spíšte každé oprávnenie, ktoré obrazovka dovolí niekomu uložiť: rola × oblasť × akcia. Tento zoznam je špecifikácia, nie vaše politiky.
- Pre každé políčko zapíšte skutočnú akciu, ktorú má riadiť, a ako sa prejaví odmietnutie (žiadne riadky, chyba, 403).
- Postavte databázu, ktorá zodpovedá produkcii, a vytvorte jej odtlačok, aby odchýlka schémy beh zastavila.
- Každú akciu spustite ako prihlásený člen, políčko vypnuté a potom zapnuté, a porovnajte.
- Jednu politiku zámerne pokazte a overte, že overenie zčervená.
- Každé políčko, ktoré nerobí nič, skryte alebo zapojte a overenie urobte bránou pred pushom.
Ak vaša aplikácia už RLS má, ale nie ste si istí, že robí to, čo hovorí obrazovka nastavení, rovnaká metóda funguje ako audit existujúceho kódu, prvý krok záchrany projektu. O projekte za týmito číslami som písal v prípadovej štúdii sledovania času pre audítorskú firmu.
Nie ste si istí, či vaše RLS zodpovedá obrazovke nastavení?
Rád si vymením skúsenosti o tom, ako ho testujete. Bez ponuky.
Ozvite sa