TL;DR
Testaa Supabasen row level security (rivitason tietoturva) niin kuin jäsen sen kohtaa: kirjaudu sisään kunkin roolin edustajana, tee oikea toiminto ensin yksi oikeus pois päältä ja sitten päällä, ja tarkista, että tulos muuttuu. Tuotantosovelluksessa, jossa oli viisi muokattavaa roolia, tämä paljasti, että 30 niistä 87 oikeusruudusta, jotka ylläpitäjä pystyi tallentamaan, ei muuttanut tietokannassa mitään. Politiikka kerrallaan etenevä testi ei näe tätä, koska se testaa kirjoittamaasi politiikkaa, ei ruutua näytöllä.
Olen Denys, full-stack- ja tekoäly-insinööri. Tänä vuonna rakensin tilintarkastusyhtiölle projekti- ja ajanseurantasovelluksen Supabasen päälle, ja jokainen käyttöoikeussääntö asuu siinä tietokannassa. Alla kuvaan menetelmän, jolla todistan nuo säännöt, mitä se löysi ja mitä sen ajaminen maksaa.
Miksi politiikkatestit eivät riitä Supabase RLS:n testaamiseen?
Politiikkatesti vastaa kysymykseen “tekeekö tämä politiikka sen, mitä kirjoitin?”. Käyttäjäsi kysyvät toista: “jos poistan tämän ruudun rastin tältä roolilta, pystyvätkö he silti tekemään sen?”. Pienessä sovelluksessa vastaus on sama. Sovelluksessa, jonka asetussivulla ylläpitäjät muokkaavat oikeusmatriisia, vastaukset ajautuvat erilleen.
Sovelluksessa oli 41 taulua, joissa RLS oli päällä, ja 402 CREATE POLICY -lausetta 255 migraatiossa. Ylläpitäjän näkymässä oli matriisi, jossa alueet (projektit, tehtävät, tuntikirjaukset, tiedostot ja niin edelleen) olivat vastakkain toimintojen (katselu, luonti, muokkaus, arkistointi) kanssa, yksi sarake roolia kohden. Jokainen tallennettu ruutu oli lupaus ylläpitäjälle. Mikään ei tarkistanut, että tietokanta piti sen.
Supabase dokumentoi vakiotyökalut hyvin: pgTAP-testit ja testiapurit antavat luoda käyttäjiä, vaihtaa niihin ja tarkistaa, mitä ne voivat lukea. Käytä niitä. Alla oleva menetelmä tulee niiden päälle: sitä ohjaa ylläpitäjän näkemä matriisi, ei politiikkojen lista.
Miten matriisitodistus toimii?
Jokaisesta matriisin ruudusta tulee koetin. Koetin kirjautuu roolin jäsenenä, tekee yhden oikean toiminnon ja kirjaa tuloksen. Se tekee tämän kahdesti: kerran ruutu pois päältä ja kerran päällä, kaikki muu ennallaan. Jos tulos muuttuu, ruutu on voimassa. Jos ei, ruutu ei tee mitään, vaikka se näyttäisi näytöllä kuinka vakuuttavalta.
- Yksi ruutu matriisissaEsimerkki: Standard Member · Tehtävät · Luonti
- Ruutu pois: toimi roolin jäsenenäLuo tehtävä samaa reittiä kuin sovellus. Kirjaa tulos.
- Ruutu päällä: toista sama toimintoSama käyttäjä, sama data. Vain ruutu muuttui.
- Vertaa kahta tulostaTuomio riippuu vain siitä, mitä tietokanta teki.
- Tulos muuttuiVoimassa. Estetty pois päältä, sallittu päällä.
- Sama molemmilla kerroillaEi tee mitään. Näyttö lupaa hallintaa, jota ei ole.
- Näyttö ja tietokanta eri mieltäBugi. Sivu näyttää painikkeen, jonka tietokanta torjuu, tai päinvastoin.
“Tulos” tarkoittaa tässä sitä, mitä oikeutta käyttävä osa tekee: politiikka palauttaa rivejä tai torjuu kirjoituksen, tietokantafunktio nostaa virheen, triggeri estää tai edge function vastaa 403. Yksi ruutu voi vartioida useampaa näistä, joten 87 ruutua vaativat 279 tarkistusta roolia kohden. Jokainen tarkistus päättyi yhteen näistä tuomioista:
| Tuomio | Tarkistuksia roolia kohden | Mitä se tarkoittaa |
|---|---|---|
| Vaikutus | 227 | Tulos kääntyi ruudun mukana |
| Kuollut | 30 | Tulos pysyi samana, oli ruutu pois tai päällä |
| Saavuttamaton | 19 | Rooli ei pääse toimintoon lainkaan, todistettu erikseen |
| Edge function | 1 | Edge function valvoo, tarkistettu HTTP:n yli |
| Vain näytöllä | 1 | Muuttaa vain sitä, mitä sivu näyttää, katettu front-end-testillä |
| Näyttö ja tietokanta samaa | 1 | Se, mitä sivu tarjoaa, vastaa sitä, mitä tietokanta sallii |
| Hylätty | 0 | Tarkistus, jonka tulos ei vastannut odotusta |
Mitä todistus löysi?
Ajo 15. syyskuuta 2026 kattoi kaikki viisi roolia, joita ylläpitäjä voi muokata (Admin, Senior, Standard, Restricted ja kloonattu oma rooli). Tulokset olivat kaikille rooleille samat.
Yksikään näistä 30:stä ei päästänyt ketään sisään. Niistä 29:ää ei lukenut yksikään politiikka, funktio tai triggeri; viimeinen lakkasi vaikuttamasta, kun politiikka kirjoitettiin uudelleen. Ongelma on silti todellinen: ylläpitäjä, joka poistaa roolilta rastin kohdasta “Poisto”, uskoo, ettei rooli voi poistaa. Piilotimme kaikki 30 sen sijaan, että olisimme kytkeneet ne toimimaan, koska jokainen olisi vaatinut tuotepäätöksen, eikä näytöllä pidä olla ruutua, joka ei tee mitään, sillä välin kun päätös tehdään.
Sama työ toi esiin neljä muuta vikaa:
| Mikä oli vialla | Miten se näkyi | Korjaus |
|---|---|---|
| 30 ruutua ei tehnyt mitään | Kuollut-tuomio, sama tulos pois ja päällä | Piilotettu matriisista |
| Kolme tietokantafunktiota päästi toiminnon läpi, kun tehtävällä ei ollut vastuuhenkilöä | Koetin, joka olisi pitänyt torjua, sallittiin | Funktiot torjuvat nyt, kun arvo puuttuu |
| Tuntikirjauksen palautus luonnokseksi riippui väärästä ruudusta | Näyttö ja tietokanta olivat eri mieltä yhden roolin kohdalla | Sidottu uudelleenavausoikeuteen, jolle se kuuluu |
| Jäsen näki uudet oikeudet vasta sivun uudelleenlatauksen jälkeen | Tietokanta oli muuttunut, sivu ei | Oikeudet päivittyvät 60 sekunnin välein ja kun ikkuna saa fokuksen |
| Sarakkeen oletusarvo rikkoi oman check-rajoitteensa, joten lisäykset ilman sitä saraketta kaatuivat | Korjauksen tietoturvakatselmointi löysi sen saman työn aikana | Oletusarvo vaihdettu arvoon, jonka rajoite sallii |
Puuttuvan vastuuhenkilön bugi oli se, jolla oli väliä. Vartijat oli kirjoitettu PL/pgSQL:llä näin:
if not (has_permission or assignee_id = auth.uid()) then
raise exception 'forbidden';
end if;
Kun assignee_id on null, assignee_id = auth.uid() on null, false or null on null, not null on edelleen null, ja if käsittelee nullia epätotena. Poikkeus ei laukea koskaan. Tuotannossa yhdelläkään tehtävällä ei ollut vastuuhenkilöä, joten kyse ei ollut reunatapauksesta: se koski jokaista rajattua tehtävää. Korjaus käärii ehdon coalesce(…, false)-kutsuun, jolloin puuttuva arvo torjutaan, ja se julkaistiin ensimmäisenä, omana muutoksenaan, ennen muuta erää.
Miten sen ajaa tuotannon kopiota vasten?
Todistus skeemaa vasten, joka poikkeaa tuotannosta, ei todista tuotannosta mitään. Testiympäristö pyörii paikallisessa Supabase-pinossa, erillisessä tietokannassa, joka on kopio tuotannon skeemasta sekä katselmoitavana olevat migraatiot. Ennen jokaista ajoa se vertaa kummankin paikallisen tietokannan sormenjälkeä: 799 riviä, jotka kuvaavat taulut, politiikat, funktiot ja oikeudet. Mikä tahansa ero pysäyttää ajon.
- Tuotannon skeemaKopioitu yhdelle luotetulle koneelle. Se ei koskaan päädy hostatulle CI-ajurille.
- Paikallinen pino: sovelluksen tietokantaPaikallinen API ja auth palvelevat sitä, ja end-to-end-tarkistukset käyttävät sitä.
- Paikallinen pino: eristetty todistustietokantaSama skeema, vain matriisitodistuksen käytössä.
- Sormenjäljen tarkistus799 riviä tietokantaa kohden. Mikä tahansa ero pysäyttää ajon ennen ensimmäistäkään koetinta.
- Koettimet ajetaan kunkin roolin nimissäOikeina sisäänkirjautuneina jäseninä, samoja reittejä kuin sovellus.
- Ei jälkiäRivimäärien lopussa on oltava samat kuin alussa.
Kaksi yksityiskohtaa teki testiympäristöstä luotettavan:
- Riko se ensin tahallaan. Ennen kuin luotin vihreään ajoon, korvasin yhden insert-politiikan sellaisella, joka hyväksyy kaiken, ja ajoin todistuksen uudelleen. Sen piti muuttua punaiseksi niiden ruutujen kohdalla, joita tuo politiikka vartioi. Testiympäristö, joka pysyy vihreänä rikkinäisen politiikan kanssa, on koriste.
- Tarkista myös tallennettu tila. Ensimmäinen kierros aloittaa jokaisen roolin puhtaasta lähtötilasta. Toinen kierros aloittaa kunkin roolin nykyisestä tallennetusta matriisista, koska ruutu voi käyttäytyä eri tavalla niiden ruutujen vieressä, jotka ylläpitäjä on oikeasti rastittanut.
Miten se pidetään vihreänä julkaisun jälkeen?
Kerran ajettu todistus vanhenee seuraavassa migraatiossa. Todistus ajetaan nyt ennen jokaista pushia, joka koskee migraatioita, auth-koodia tai itse matriisia.
- git pushMuutos koskee migraatioita, authia tai matriisia.
- Pre-push-hook: matriisitodistusAjetaan paikallisesti todistustietokantaa vasten. Punainen estää pushin.
- Push päätyy mainiinJulkaisu etenee normaalisti.
- Itse ylläpidetty CI-ajoSama todistus uudelleen koneella, jolla tuotannon skeeman kopio on.
Todistus on 135 testiä 11 tiedostossa, ja samassa projektissa on niiden rinnalla noin 3 100 tavallista yksikkötestiä.
Mikä ei toiminut?
- Hostattu CI ei pystynyt ajamaan sitä. Todistus tarvitsee tuotannon skeeman kopion, enkä halunnut sitä hostatulle ajurille. Siksi estävä portti on pre-push-hook, ja CI itse ylläpidetyllä koneella on toinen tarkistus, jota julkaisu ei odota.
- Kiinteät rivimäärät hajosivat. Ensimmäinen “ei jälkiä” -tarkistus vertasi kiinteisiin lukuihin. Seuraava täysin asiallinen migraatio lisäsi oikeusrivin ja muutti sen punaiseksi. Nyt se vertaa lopun rivimääriä alun rivimääriin.
- Ohitettu ei ole läpäisty. End-to-end-tarkistukset tarvitsevat paikallisen edge-ajoympäristön. Kun se on alhaalla, ne ohitetaan, ja ohituksen lukee helposti vihreäksi.
- Se on hidas kuormitetulla läppärillä. Roolikohtaiset kierrokset tarvitsivat pidemmät aikakatkaisut kuormitetulla koneella.
- Se todistaa sen, mitä koettimet kuvaavat. Ruutu, jolle kukaan ei kirjoittanut koetinta, raportoidaan kattavuusaukkona, ei turvallisena.
Näin toistat tämän omassa Supabase-sovelluksessasi
- Listaa jokainen oikeus, jonka näyttö antaa jonkun tallentaa: rooli × alue × toiminto. Tuo lista on spesifikaatio, eivät politiikkasi.
- Kirjaa jokaiselle ruudulle oikea toiminto, jota sen pitäisi ohjata, ja miten torjunta näkyy (ei rivejä, virhe, 403).
- Rakenna tietokanta, joka vastaa tuotantoa, ja ota siitä sormenjälki, jotta skeeman ajautuminen pysäyttää ajon.
- Aja jokainen toiminto sisäänkirjautuneena jäsenenä, ruutu ensin pois ja sitten päällä, ja vertaa.
- Riko yksi politiikka tahallaan ja varmista, että todistus muuttuu punaiseksi.
- Piilota tai kytke jokainen ruutu, joka ei tee mitään, ja tee todistuksesta portti ennen pushia.
Jos sovelluksessasi on jo RLS, mutta et ole varma, tekeekö se sen, mitä asetussivu sanoo, sama menetelmä toimii olemassa olevan koodikannan tarkastuksena, ja se on projektin pelastuksen ensimmäinen vaihe. Kirjoitin näiden lukujen taustalla olevasta projektista tilintarkastusyhtiön ajanseurannan tapauksessa.
Et ole varma, vastaako RLS asetussivuasi?
Vertaan mielelläni kokemuksia siitä, miten testaat sitä. Ei myyntipuhetta.
Ota yhteyttä