Pereiti prie turinio

Supabase RLS policy: pavyzdžiai, išbandyti per API

Žiūrėti kaip MarkdownAtidaryti per ClaudeAtidaryti per ChatGPTAtidaryti per Perplexity

Klausimas taip pat nukopijuojamas. Jei pokalbis atsidaro tuščias, įklijuokite jį.

TL;DR

Žemiau pateiktos RLS politikos, kurių reikia daugumai Supabase programų. Kiekvieną paleidau per REST API kaip svečias, kaip eilutės savininkas ir kaip kitas prisijungęs vartotojas. Trys rezultatai verti dėmesio prieš ką nors kopijuojant: svetimos eilutės update grąžina 200 su tuščiu sąrašu, o ne klaidą; nekaltai atrodanti insert politika narystės lentelėje leido vartotojui prisijungti prie kitos įmonės ir skaityti jos projektus; o užrašius (select auth.uid()) vietoj auth.uid(), viena užklausa 200 000 eilučių lentelėje pagreitėjo nuo 156 ms iki 7,4 ms.

Esu Denys, full-stack ir AI inžinierius, ir dauguma programų, kurias kuriu ar taisau, veikia ant Supabase. Pavyzdžių, kaip parašyti Supabase RLS policy, yra visur; ką jos daro, kai pasiekia tikra užklausa, parodoma retai. Todėl jas paleidau.

Kaip testavau. Dauguma užklausų ėjo per vietinį Supabase stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) su publishable key ir dviem tikrais prisijungusiais vartotojais, A ir B. Atvejai, kuriems HTTP nereikia, pavyzdžiui, sintaksės klaidos ir laikai, veikė ant paprasto vietinio Postgres 17.10 su Supabase rolėmis anon ir authenticated bei iš naujo sukurta funkcija auth.uid(). Viskas 2026 m. spalio 24 d., su išgalvotomis eilutėmis.

Prieš bet kokią politiką: ar lentelė turi teisę (grant)?

Politika nusprendžia, kurias eilutes rolė mato. Teisė (grant) nusprendžia, ar rolė apskritai pasiekia lentelę, o dabartiniuose Supabase projektuose naujos lentelės Data API teisių automatiškai nebegauna (nuo 2026 m. spalio 30 d. visuose projektuose). Mano stack lentelė su įjungtu RLS ir be teisės atsakė taip:

Užklausa Statusas Turinys
Svečias GET /invoices 401 permission denied for table invoices, su užuomina GRANT SELECT ... TO anon
Vartotojas A GET /invoices 403 tas pats, su užuomina authenticated

Taigi pirmas žingsnis kiekvienai lentelei, kurią programa skaito iš naršyklės:

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

Suteikite tik tas komandas, kurių reikia naršyklei, ir tik rolei authenticated, nebent lentelę turi skaityti ir svečiai.

Kaip parašyti Supabase RLS policy, kad vartotojai matytų tik savo eilutes?

Viena politika kiekvienai komandai, kiekviena apribota prisijungusiais vartotojais:

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 turi dvi eilutes, B vieną. Kas grįžo:

Užklausos autorius Ką daro Statusas Turinys
Svečias skaityti viską 401 permission denied (anon neturi teisės)
A skaityti viską 200 2 vartotojo A eilutės
A įterpti eilutę, priklausančią B 403 new row violates row-level security policy
A atnaujinti vartotojo B eilutę 200 []
A ištrinti vartotojo B eilutę 200 []
A pakeisti savo eilutės savininką į B 403 new row violates row-level security policy

Du 200 [] įrašai yra tie, kuriuos verta įsiminti. RLS neatmeta update ar delete eilučių, kurių nematote; jis jas išfiltruoja, o nulis pakeistų eilučių yra sėkmė. Jei jūsų programa „nėra klaidos“ laiko „išsaugota“, ji pasakys vartotojui, kad pakeitimas pavyko, nors nieko neįvyko. Paprašykite pakeistų eilučių (.select() supabase-js, Prefer: return=representation per HTTP) ir patikrinkite, ar viena grįžo.

Kodėl update politikai reikia WITH CHECK?

using parenka, kurių esamų eilučių vartotojas gali liesti; with check nusprendžia, kaip tos eilutės gali atrodyti po to. Palyginau tris tos pačios update politikos versijas:

Update politika A atiduoda savo eilutę vartotojui B
using (uid = user_id) with check (uid = user_id) 403, atmesta
using (uid = user_id), be with check atmesta (Postgres naudoja using kaip patikrą)
using (uid = user_id) with check (true) 200, eilutė dabar priklauso B

Praleisti with check saugiau, nei atrodo, nes Postgres pasitelkia using išraišką. with check (true), kurį kai kurie generatoriai rašo, kad „leistų update“, būtent ir leidžia vartotojui perkelti eilutę į svetimą paskyrą. Parašykite patikrą pilnai.

Kaip padaryti profilius viešus, bet redaguojamus tik savininko?

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);

Paleidimas ant paprasto Postgres: svečias perskaitė abu profilius, svečio update pakeitė 0 eilučių, o A update profiles set display_name = 'x' be filtro pakeitė lygiai 1 eilutę, paties A. using (true) tinka select duomenims, kurie ir turi būti vieši. Bet kurioje kitoje lentelėje tai tas pats, kas jokio RLS, o politika, parašyta be to sąlygos, galioja ir svečiams: mano svečias perskaitė „privačią“ eilutę per using (true) politiką, kuri neturėjo to.

Kaip dalytis eilutėmis komandos viduje?

Įprasta forma: lentelė memberships ir politika duomenyse, kuri joje ieško vartotojo komandų.

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())));

Ta politika tvarkinga. Nutekėjimas buvo šalia. Lentelei memberships daviau politiką, kurią siūlo daug pamokų, „vartotojai gali įterpti savo narystę“:

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

Ji tikrina, kam eilutė skirta, o ne kuriai komandai. A, 1 įmonės narys, išsiuntė vieną užklausą:

Viena insert politika lentelėje memberships. 1 įmonės vartotojas A per REST API. Politika: user_id = auth.uid(): Vartotojas A → jungiasi prie 2 įmonės → 201 Created → skaito B algas; Be insert politikos: Vartotojas A → jungiasi prie 2 įmonės → atmesta → tik savi projektai.
1 pav. Mano testas vietiniame Supabase stack (CLI 2.109.1), 2026 m. spalio 24 d.
A užklausa Statusas Turinys
skaityti projektus 200 A roadmap
įterpti narystę {user_id: A, org_id: 2} 201 sukurta
vėl skaityti projektus 200 A roadmap, B payroll

Sprendimas: memberships nedaryti jokios insert politikos, kad naršyklė negalėtų į ją rašyti (ant Postgres paleidimo tas pats insert tada buvo atmestas su 42501), o narius pridėti per kvietimų srautą, veikiantį serveryje arba funkcijoje, tikrinančioje kvietimą. Vartotojas, priklausantis dviem įmonėms, yra dar vieni spąstai; apie tai rašiau straipsnyje Supabase RLS: kodėl dviejų org. narys mato svetimus duomenis.

Ką daryti, jei politikai reikia skaityti savo lentelę?

„Nariai mato savo komandos draugus“ reiškia politiką lentelėje memberships, kuri skaito memberships. Postgres ją atmeta vykdant užklausą:

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

Paieškos veiksmą perkelkite į funkciją, kuri veikia kaip jos savininkas, schemoje, kurios API neatveria, su fiksuotu search_path:

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()));

Po to A matė abu 1 įmonės narius, o B matė tik save. Funkciją laikykite ne public: viską, kas ten, galima iškviesti per API.

Kaip reikalauti MFA jautrioms eilutėms?

Prie įprastos politikos pridėkite ribojančią (restrictive) politiką. Ribojančios politikos jungiamos per and, todėl turi praeiti abi:

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

Su tuo pačiu vartotoju aal1 sesija perskaitė 0 išmokų, o aal2 sesija perskaitė 1.

Kurios lentelės turi turėti įjungtą RLS ir nė vienos politikos?

Lentelės, kurias liečia tik jūsų serveris. Įjungtas RLS be politikos viską uždraudžia naršyklės rolėms: ant Postgres paleidimo svečias ir A perskaitė 0 eilučių, o A insert buvo atmestas su 42501. Palikti RLS išjungtą yra priešingybė. Lentelė su išjungtu RLS ir numatytosiomis teisėmis, skaitoma su publishable key ir be prisijungimo:

Svečio užklausa Statusas Turinys
GET /leads 200 abi eilutės
DELETE /leads?id=gt.0 200 abi eilutės, ištrintos

Būtent apie tai ir yra Advisor įspėjimas „RLS disabled in public“.

Ar viena politika gali apimti kelias komandas?

Ne sąrašu. for update, delete yra sintaksės klaida (42601: syntax error at or near ","), nors vis dar pasitaiko paskelbtose pavyzdžių rinkiniuose. for all galioja ir taiko vieną using bei vieną with check kiekvienai komandai, ko retai norite. Insert politika su using taip pat nepavyksta: only WITH CHECK expression allowed for INSERT.

Ar politikos lėtina užklausas?

Gali. Politikos išraiška vykdoma kiekvienai eilutei, kurią svarsto Postgres. Išmatavau select * from big kaip vartotojas A, 200 000 eilučių, priklausančių 1 000 vartotojų, A turėjo 200 (7 paleidimų mediana, Postgres 17.10):

Viena tik savininko politika, keturi užrašymai. Milisekundės 200 savų eilučių iš 200 000 perskaityti. 7 paleidimų mediana. auth.uid() = user_id, be indekso: 156,30; (select auth.uid()), be indekso: 7,40; auth.uid() = user_id, su indeksu: 0,21; (select auth.uid()), su indeksu: 0,05.
2 pav. Mano testas vietiniame Postgres 17.10 su atkurtomis Supabase rolėmis, 2026 m. spalio 24 d.
Politika ir indeksas Laiko mediana
auth.uid() = user_id, be indekso 156,3 ms
(select auth.uid()) = user_id, be indekso 7,4 ms
auth.uid() = user_id, indeksas ties user_id 0,21 ms
(select auth.uid()) = user_id, indeksas 0,05 ms

Apvyniojus iškvietimą select, Postgres apskaičiuoja reikšmę vieną kartą per užklausą, o ne vieną kartą per eilutę; likusį darbą padaro indeksas. Supabase RLS našumo vadovas nurodo tokio paties dydžio pokytį savo 100 000 eilučių lentelėje (nuo 179 ms iki 9 ms). Daug tinklaraščių tuos skaičius cituoja kaip savo; aukščiau pateikti yra iš mano paleidimo.

Iš kur turi būti imamos rolės?

Iš app_metadata arba savo lentelės, niekada iš user_metadata. Prisijungęs vartotojas gali pakeisti savo user_metadata per supabase.auth.updateUser(), todėl politika, tikrinanti auth.jwt() -> 'user_metadata' ->> 'role', leidžia jam pačiam suteikti sau rolę. Tą patį daro registracijos trigeris, kuris kopijuoja rolę iš registracijos metaduomenų; tas atvejis aprašytas straipsnyje Supabase raw_user_meta_data: kaip registracija tapo Owner.

Kaip patikrinti savo politikas?

Apribojimai. Vienas vietinis stack ir vienas nešiojamasis kompiuteris, išgalvoti duomenys, 2026 m. spalio 24 d. Laikai priklauso nuo aparatūros ir lentelės formos; svarbiau santykiai nei milisekundės. Storage politikos ir Realtime čia neapimti.

Nesate tikri, ar jūsų politikos laikosi?

Mielai kartu pažiūrėsiu į schemą. Be pardavimų kalbų.

Susisiekti

© 2026 Denys Zaichenko. Originalus straipsnis: https://madsalt.tech/lt/blog/supabase-rls-policy-examples/