Siirry sisältöön

Supabase RLS policy -esimerkit testattuna API:n kautta

Näytä MarkdowninaAvaa palvelussa ClaudeAvaa palvelussa ChatGPTAvaa palvelussa Perplexity

Kysymys kopioidaan myös. Jos keskustelu avautuu tyhjänä, liitä se.

TL;DR

Alla ovat RLS-politiikat, joita useimmat Supabase-sovellukset tarvitsevat, ja jokainen on ajettu REST API:n kautta vieraana, rivin omistajana ja toisena kirjautuneena käyttäjänä. Kolme tulosta kannattaa tietää ennen kuin kopioit mitään: toisen käyttäjän rivin update palauttaa 200 ja tyhjän listan, ei virhettä; harmittoman näköinen insert-politiikka jäsentaulussa antoi käyttäjän liittyä toiseen yritykseen ja lukea sen projektit; ja kun kirjoitin (select auth.uid()) lauseen auth.uid() sijaan, yksi kysely nopeutui 156 ms:sta 7,4 ms:iin 200 000 rivillä.

Olen Denys, full-stack- ja tekoälyinsinööri, ja suurin osa rakentamistani tai korjaamistani sovelluksista pyörii Supabasella. Politiikkojen koodinpätkiä on kaikkialla, mutta se, mitä ne tekevät, kun oikea pyyntö osuu niihin, näytetään harvoin. Joten ajoin ne. Tässä ovat Supabase RLS policy -esimerkit, joista aloitan itse, ja kunkin tulos.

Näin testasin. Suurin osa pyynnöistä kulki paikallisen Supabase-stackin läpi (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) julkisella avaimella ja kahdella oikealla kirjautuneella käyttäjällä, A:lla ja B:llä. Tapaukset, jotka eivät tarvitse HTTP:tä, kuten syntaksivirheet ja ajoitukset, ajoin tavallisella paikallisella Postgres 17.10:llä, jossa Supabasen anon- ja authenticated-roolit ja auth.uid() oli luotu uudelleen. Kaikki 24. lokakuuta 2026, keksityillä riveillä.

Ennen yhtäkään politiikkaa: onko taululla grant?

Politiikka päättää, mitkä rivit rooli näkee. Grant päättää, pääseekö rooli taulun luo lainkaan, ja nykyisissä Supabase-projekteissa uudet taulut eivät enää saa Data API -granteja automaattisesti (30. lokakuuta 2026 alkaen kaikissa projekteissa). Omassa stackissani taulu, jossa RLS oli päällä mutta granttia ei ollut, vastasi:

Pyyntö Tila Runko
Vieras GET /invoices 401 permission denied for table invoices, vihjeenä GRANT SELECT ... TO anon
Käyttäjä A GET /invoices 403 sama, vihjeenä authenticated-roolin grant

Joten jokaiselle taululle, jota sovellus lukee selaimesta, ensimmäinen askel on:

alter table public.todos enable row level security;
grant select, insert, update, delete on public.todos to authenticated;

Anna vain ne komennot, joita selain tarvitsee, ja vain roolille authenticated, ellei vieraiden tarvitse lukea taulua.

Miten käyttäjät näkevät vain omat rivinsä?

Yksi politiikka komentoa kohden, jokainen rajattu kirjautuneisiin käyttäjiin:

create policy "read own" on public.todos
  for select to authenticated using ((select auth.uid()) = user_id);
create policy "add own" on public.todos
  for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "edit own" on public.todos
  for update to authenticated
  using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "delete own" on public.todos
  for delete to authenticated using ((select auth.uid()) = user_id);

A omistaa kaksi riviä, B yhden. Tulokset:

Pyytäjä Mitä Tila Runko
Vieras lue kaikki 401 permission denied (anon-roolilla ei grantia)
A lue kaikki 200 A:n 2 riviä
A insert B:n omistamaan riviin 403 new row violates row-level security policy
A update B:n riviin 200 []
A delete B:n riviin 200 []
A vaihda oman rivin omistajaksi B 403 new row violates row-level security policy

Kaksi 200 [] -riviä kannattaa muistaa. RLS ei hylkää updatea tai deleteä riveille, joita et näe; se suodattaa ne pois, ja nolla muuttunutta riviä on onnistuminen. Jos sovelluksesi pitää “ei virhettä” merkkinä siitä, että tallennus onnistui, se kertoo käyttäjälle muutoksen onnistuneen, vaikka mitään ei tapahtunut. Pyydä muuttuneet rivit takaisin (.select() supabase-js:ssä, Prefer: return=representation HTTP:llä) ja tarkista, että yksi palasi.

Miksi update-politiikka tarvitsee WITH CHECKin?

using valitsee, mihin olemassa oleviin riveihin käyttäjä saa koskea; with check päättää, miltä rivit saavat näyttää jälkeenpäin. Vertasin saman update-politiikan kolmea versiota:

Update-politiikka A antaa oman rivinsä B:lle
using (uid = user_id) with check (uid = user_id) 403, estetty
using (uid = user_id), ei with check -osaa estetty (Postgres käyttää using-ehtoa myös checkinä)
using (uid = user_id) with check (true) 200, rivi kuuluu nyt B:lle

with checkin jättäminen pois on turvallisempaa kuin miltä näyttää, koska Postgres palaa using-lausekkeeseen. Käyttäjän päästää siirtämään rivin jonkun toisen tilille with check (true), jonka jotkin generaattorit kirjoittavat “sallimaan updatet”. Kirjoita check kokonaan.

Miten profiilit tehdään julkisiksi mutta vain omistajan muokattaviksi?

create policy "public read" on public.profiles
  for select to anon, authenticated using (true);
create policy "own edit" on public.profiles
  for update to authenticated
  using ((select auth.uid()) = id) with check ((select auth.uid()) = id);

Tavallisella Postgresilla: vieras luki molemmat profiilit, vieraan update muutti 0 riviä, ja A:n update profiles set display_name = 'x' ilman suodatinta muutti täsmälleen 1 rivin, A:n oman. using (true) sopii selectiin datalle, jonka on tarkoitus olla julkista. Millä tahansa muulla taululla se vastaa sitä, ettei RLS:ää ole, ja ilman to-lausetta kirjoitettu politiikka koskee myös vieraita: vieraani luki “yksityisen” rivin using (true) -politiikalla, jossa ei ollut to-osaa.

Miten rivejä jaetaan tiimin sisällä?

Tavallinen muoto: memberships-taulu ja datan politiikka, joka hakee käyttäjän tiimit sieltä.

create policy "members read" on public.projects
  for select to authenticated
  using (org_id in (select org_id from public.memberships
                    where user_id = (select auth.uid())));

Tuo politiikka on kunnossa. Vuoto oli sen vieressä. Annoin memberships-taululle politiikan, jota monet oppaat ehdottavat, “käyttäjät voivat lisätä oman jäsenyytensä”:

create policy "join any org" on public.memberships
  for insert to authenticated with check (user_id = (select auth.uid()));

Se tarkistaa, kenelle rivi on, ei mihin tiimiin. A, yrityksen 1 jäsen, lähetti yhden pyynnön:

Yksi insert-politiikka taulussa memberships. Yrityksen 1 käyttäjä A, REST API:n kautta. Politiikka: user_id = auth.uid(): Käyttäjä A → liittyy yritykseen 2 → 201 Created → lukee B:n palkat; Ei insert-politiikkaa: Käyttäjä A → liittyy yritykseen 2 → estetty → vain omat projektit.
Kuva 1. Oma testini paikallisella Supabase-stackilla (CLI 2.109.1), 24. lokakuuta 2026.
A:n pyyntö Tila Runko
lue projektit 200 A roadmap
insert jäsenyys {user_id: A, org_id: 2} 201 luotu
lue projektit uudelleen 200 A roadmap, B payroll

Korjaus on olla antamatta memberships-taululle lainkaan insert-politiikkaa, jolloin selain ei voi kirjoittaa siihen (tavallisella Postgresilla sama insert estettiin silloin virheellä 42501), ja lisätä jäsenet kutsuvirran kautta, joka ajetaan palvelimella tai funktiossa, joka tarkistaa kutsun. Käyttäjä, joka kuuluu kahteen yritykseen, on seuraava ansa; kirjoitin siitä artikkelissa Supabase RLS: kahden organisaation käyttäjä vuotaa dataa.

Entä jos politiikan pitää lukea omaa taulunsa?

“Jäsenet näkevät tiimikaverinsa” tarkoittaa politiikkaa taulussa memberships, joka lukee taulua memberships. Postgres kieltäytyy kyselyn ajon aikana:

ERROR 42P17: infinite recursion detected in policy for relation "members2"

Siirrä haku funktioon, joka ajetaan omistajansa oikeuksilla, skeemassa, jota API ei julkaise, kiinteällä search_pathilla:

create schema private;
create function private.user_org_ids() returns setof int
  language sql stable security definer set search_path = ''
  as $$ select org_id from public.members2 where user_id = (select auth.uid()) $$;
revoke all on function private.user_org_ids() from public;
grant usage on schema private to authenticated;
grant execute on function private.user_org_ids() to authenticated;

create policy "see teammates" on public.members2
  for select to authenticated using (org_id in (select private.user_org_ids()));

Sen jälkeen A näki yrityksen 1 molemmat jäsenet ja B näki vain itsensä. Pidä funktio poissa skeemasta public: kaikki siellä oleva on kutsuttavissa API:n kautta.

Miten MFA vaaditaan arkaluonteisille riveille?

Lisää rajoittava politiikka tavallisen päälle. Rajoittavat politiikat yhdistetään and-operaattorilla, joten molempien on mentävä läpi:

create policy "mfa only" on public.payouts as restrictive
  for select to authenticated using ((select auth.jwt() ->> 'aal') = 'aal2');

Samalla käyttäjällä aal1-istunto luki 0 maksua ja aal2-istunto luki 1.

Millä tauluilla RLS on päällä ilman politiikkaa?

Tauluilla, joihin koskee vain palvelimesi. RLS päällä ilman politiikkaa kieltää kaiken selaimen rooleilta: tavallisella Postgresilla vieras ja A lukivat 0 riviä, ja A:n insert estettiin virheellä 42501. RLS:n jättäminen pois päältä on päinvastainen tilanne. Taulu, jossa RLS on pois päältä ja oletusgrantit, luettuna julkisella avaimella ilman kirjautumista:

Vieraan pyyntö Tila Runko
GET /leads 200 molemmat rivit
DELETE /leads?id=gt.0 200 molemmat rivit, poistettu

Tästä Advisorin varoituksessa “RLS disabled in public” on kyse.

Voiko yksi politiikka kattaa useita komentoja?

Ei listalla. for update, delete on syntaksivirhe (42601: syntax error at or near ","), vaikka se esiintyy yhä julkaistuissa koodinpätkäkokoelmissa. for all on kelvollinen ja soveltaa yhtä using- ja yhtä with check -ehtoa kaikkiin komentoihin, mitä harvoin halutaan. Myös insert-politiikka, jolla on using, epäonnistuu: only WITH CHECK expression allowed for INSERT.

Hidastavatko politiikat kyselyjä?

Voivat hidastaa. Politiikan lauseke ajetaan jokaiselle riville, jota Postgres harkitsee. Ajoitin select * from big käyttäjänä A 200 000 rivillä, jotka omistaa 1 000 käyttäjää, A:lla niistä 200 (7 ajon mediaani, Postgres 17.10):

Yksi omistajapolitiikka, neljä kirjoitustapaa. Millisekuntia 200 oman rivin lukuun 200 000:sta. 7 ajon mediaani. auth.uid() = user_id, ei indeksiä: 156,30; (select auth.uid()), ei indeksiä: 7,40; auth.uid() = user_id, indeksillä: 0,21; (select auth.uid()), indeksillä: 0,05.
Kuva 2. Oma testini paikallisella Postgres 17.10:llä, jossa Supabasen roolit oli luotu uudelleen, 24. lokakuuta 2026.
Politiikka ja indeksi Mediaani
auth.uid() = user_id, ei indeksiä 156,3 ms
(select auth.uid()) = user_id, ei indeksiä 7,4 ms
auth.uid() = user_id, indeksi sarakkeella user_id 0,21 ms
(select auth.uid()) = user_id, indeksi 0,05 ms

Kutsun kietominen select-lauseeseen antaa Postgresin laskea sen kerran kyselyä kohden eikä kerran riviä kohden; indeksi hoitaa lopun. Supabasen RLS-suorituskykyopas raportoi saman suuruusluokan muutoksen omassa 100 000 rivin taulussaan (179 ms:sta 9 ms:iin). Moni blogikirjoitus siteeraa nuo luvut ominaan; yllä olevat ovat omasta ajostani.

Mistä roolien pitäisi tulla?

Kentästä app_metadata tai omasta taulustasi, ei koskaan kentästä user_metadata. Kirjautunut käyttäjä voi muuttaa oman user_metadatansa komennolla supabase.auth.updateUser(), joten politiikka, joka tarkistaa auth.jwt() -> 'user_metadata' ->> 'role', antaa hänen myöntää roolin itselleen. Rekisteröitymistrigger, joka kopioi roolin rekisteröitymisen metadatasta, tekee saman; se tapaus on artikkelissa Supabase raw_user_meta_data: rekisteröityjästä tuli Owner.

Miten tarkistat omat politiikkasi?

  • Ilmainen Supabasen tietoturvatarkistus lukee projektisi vain luku -tilassa ja liputtaa aina tosi olevat politiikat, vieraiden kirjoituspolitiikat, käyttäjän metadataan luottavat politiikat ja taulut, joilla RLS on pois päältä, kullekin korjaus-SQL:llä.
  • Mikään skanneri ei tiedä, mitkä rivit kuuluvat kenellekin. Sitä varten aja pyyntöjä kahtena käyttäjänä, kuten tämä artikkeli tekee; koko menetelmä rooleja käyttävälle sovellukselle on artikkelissa Supabase RLS:n testaus.
  • Mitä Supabase suojaa puolestasi ja mitä se jättää sinulle, löytyy artikkelista onko Supabase turvallinen?.
  • Samat omistaja- ja tiimitestit ajettuna myös Appwritella löytyvät artikkelista Appwrite vs Supabase tietoturva: samat hyökkäykset.

Rajoitukset. Yksi paikallinen stack ja yksi kannettava, keksittyä dataa, 24. lokakuuta 2026. Ajoitukset riippuvat laitteistosta ja taulun muodosta; suhteet ovat tärkeämpiä kuin millisekunnit. Storagen politiikkoja ja Realtimea ei käsitellä tässä.

Etkö ole varma, kestävätkö politiikkasi?

Katson mielelläni skeemaa kanssasi. Ei myyntipuhetta.

Ota yhteyttä

© 2026 Denys Zaichenko. Alkuperäinen artikkeli: https://madsalt.tech/fi/blog/supabase-rls-policy-examples/