Pāriet uz saturu

Supabase RLS policy piemēri, pārbaudīti caur API

Skatīt kā MarkdownAtvērt pakalpojumā ClaudeAtvērt pakalpojumā ChatGPTAtvērt pakalpojumā Perplexity

Jautājums tiek arī nokopēts. Ja tērzēšana atveras tukša, ielīmējiet to.

Īsumā

Zemāk ir RLS politikas, kas vajadzīgas lielākajai daļai Supabase lietotņu, un katra palaista caur REST API kā viesis, kā rindas īpašnieks un kā cits pierakstījies lietotājs. Pirms kaut ko kopējat, vērts zināt trīs rezultātus: cita lietotāja rindas update atgriež 200 ar tukšu sarakstu, nevis kļūdu; nevainīgi izskatīga insert politika dalības tabulā ļāva lietotājam pievienoties citam uzņēmumam un lasīt tā projektus; un, rakstot (select auth.uid()) auth.uid() vietā, viens vaicājums 200 000 rindu tabulā kļuva ātrāks no 156 ms uz 7,4 ms.

Esmu Denys, full-stack un mākslīgā intelekta inženieris, un lielākā daļa lietotņu, ko veidoju vai labāju, darbojas uz Supabase. Politiku fragmentu ir visur; ko tie dara, kad tiem trāpa īsts pieprasījums, reti parāda. Tāpēc es tos palaidu. Šie ir Supabase RLS policy piemēri, ar kuriem pats sāku, kopā ar katra rezultātu.

Kā testēju. Lielākā daļa pieprasījumu gāja caur lokālu Supabase stekku (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) ar publisko atslēgu un diviem īstiem pierakstītiem lietotājiem, A un B. Gadījumus, kam HTTP nav vajadzīgs, piemēram, sintakses kļūdas un laika mērījumus, palaidu parastā lokālā Postgres 17.10 ar Supabase lomām anon un authenticated un no jauna izveidotu auth.uid(). Viss 2026. gada 24. oktobrī, ar izdomātām rindām.

Pirms jebkuras politikas: vai tabulai ir grant?

Politika nosaka, kādas rindas loma redz. Grant nosaka, vai loma vispār tiek pie tabulas, un pašreizējos Supabase projektos jaunas tabulas Data API grantus vairs automātiski nesaņem (visos projektos no 2026. gada 30. oktobra). Manā stekā tabula ar ieslēgtu RLS un bez granta atbildēja šādi:

Pieprasījums Statuss Saturs
Viesis GET /invoices 401 permission denied for table invoices, ar norādi GRANT SELECT ... TO anon
Lietotājs A GET /invoices 403 tas pats, ar norādi lomai authenticated

Tātad katrai tabulai, ko lietotne lasa no pārlūka, pirmais solis ir:

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

Piešķiriet tikai tās komandas, kas pārlūkam vajadzīgas, un tikai lomai authenticated, ja vien viesiem tabula nav jālasa.

Kā panākt, lai lietotāji redz tikai savas rindas?

Viena politika katrai komandai, katra ierobežota ar pierakstītiem lietotājiem:

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 pieder divas rindas, B — viena. Kas atgriezās:

Pieprasītājs Ko Statuss Saturs
Viesis lasīt visu 401 permission denied (anon nav granta)
A lasīt visu 200 A divas rindas
A insert B piederošu rindu 403 new row violates row-level security policy
A update B rindai 200 []
A delete B rindai 200 []
A mainīt savas rindas īpašnieku uz B 403 new row violates row-level security policy

Divas 200 [] rindas ir tās, kas jāatceras. RLS neatsaka update vai delete rindām, ko jūs neredzat; tas tās izfiltrē, un nulle mainītu rindu ir veiksme. Ja jūsu lietotne uzskata “nav kļūdas” par “saglabāts”, tā pateiks lietotājam, ka labojums izdevies, kaut gan nekas nenotika. Pieprasiet mainītās rindas atpakaļ (.select() supabase-js, Prefer: return=representation caur HTTP) un pārbaudiet, vai atgriezās viena.

Kāpēc update politikai vajadzīgs WITH CHECK?

using izvēlas, kurām esošajām rindām lietotājs drīkst pieskarties; with check nosaka, kādām šīm rindām pēc tam jāizskatās. Salīdzināju trīs vienas update politikas versijas:

Update politika A atdod savu rindu B
using (uid = user_id) with check (uid = user_id) 403, atteikts
using (uid = user_id), bez with check atteikts (Postgres izmanto using arī kā check)
using (uid = user_id) with check (true) 200, rinda tagad pieder B

with check izlaišana ir drošāka, nekā šķiet, jo Postgres atgriežas pie using izteiksmes. Lietotājam ļauj pārcelt rindu uz svešu kontu with check (true), ko daži ģeneratori raksta, lai “atļautu update”. Uzrakstiet check pilnībā.

Kā padarīt profilus publiskus, bet rediģējamus tikai īpašniekam?

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

Parastā Postgres palaišanā: viesis izlasīja abus profilus, viesa update izmainīja 0 rindu, un A update profiles set display_name = 'x' bez filtra izmainīja tieši 1 rindu, paša A rindu. using (true) der select datiem, kam jābūt publiskiem. Jebkurai citai tabulai tas ir tas pats, kas bez RLS, un politika, kas uzrakstīta bez to klauzulas, attiecas arī uz viesiem: mans viesis izlasīja “privāto” rindu caur using (true) politiku bez to.

Kā koplietot rindas komandas ietvaros?

Parastā forma: tabula memberships un datu politika, kas tajā meklē lietotāja komandas.

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

Ar šo politiku viss kārtībā. Noplūde bija blakus. Tabulai memberships uzliku politiku, ko iesaka daudzas pamācības, “lietotāji var ievietot savu dalību”:

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

Tā pārbauda, kam rinda paredzēta, nevis kurai komandai. A, uzņēmuma 1 dalībnieks, nosūtīja vienu pieprasījumu:

Viena insert politika tabulā memberships. Uzņēmuma 1 lietotājs A, caur REST API. Politika: user_id = auth.uid(): Lietotājs A → pievienojas uzņ. 2 → 201 Created → lasa B algas; Nav insert politikas: Lietotājs A → pievienojas uzņ. 2 → atteikts → tikai savi projekti.
1. attēls. Mans tests lokālā Supabase stekā (CLI 2.109.1), 2026. gada 24. oktobrī.
A pieprasījums Statuss Saturs
lasīt projektus 200 A roadmap
insert dalība {user_id: A, org_id: 2} 201 izveidota
lasīt projektus vēlreiz 200 A roadmap, B payroll

Risinājums ir vispār nedot tabulai memberships insert politiku, lai pārlūks tajā nevarētu rakstīt (parastā Postgres palaišanā tas pats insert tad tika atteikts ar 42501), un dalībniekus pievienot ar uzaicinājuma plūsmu, kas darbojas serverī vai funkcijā, kas pārbauda uzaicinājumu. Lietotājs, kas pieder diviem uzņēmumiem, ir nākamās lamatas; par to rakstīju pie Supabase RLS: kāpēc divu org. lietotājs redz svešus datus.

Ko darīt, ja politikai jāizlasa sava tabula?

“Dalībnieki redz savus komandas biedrus” nozīmē politiku tabulā memberships, kas lasa tabulu memberships. Postgres atsakās vaicājuma izpildes laikā:

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

Pārvietojiet meklēšanu uz funkciju, kas darbojas ar īpašnieka tiesībām, shēmā, ko API nepublicē, ar fiksētu 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()));

Pēc tam A redzēja abus uzņēmuma 1 dalībniekus, bet B redzēja tikai sevi. Turiet funkciju ārpus public: viss, kas tur ir, ir izsaucams caur API.

Kā pieprasīt MFA jutīgām rindām?

Pievienojiet ierobežojošu politiku virs parastās. Ierobežojošās politikas apvieno ar and, tāpēc jāiziet abām:

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

Ar to pašu lietotāju aal1 sesija izlasīja 0 izmaksu, bet aal2 sesija — 1.

Kurām tabulām RLS jābūt ieslēgtam un politikas nav?

Tabulām, kam pieskaras tikai jūsu serveris. Ieslēgts RLS bez politikas liedz visu pārlūka lomām: parastā Postgres palaišanā viesis un A izlasīja 0 rindu, un A insert tika atteikts ar 42501. RLS atstāšana izslēgtu ir pretējais. Tabula ar izslēgtu RLS un noklusējuma grantiem, lasīta ar publisko atslēgu bez pierakstīšanās:

Viesa pieprasījums Statuss Saturs
GET /leads 200 abas rindas
DELETE /leads?id=gt.0 200 abas rindas, izdzēstas

Tieši par to ir Advisor brīdinājums “RLS disabled in public”.

Vai viena politika var aptvert vairākas komandas?

Ne ar sarakstu. for update, delete ir sintakses kļūda (42601: syntax error at or near ","), kaut gan tā joprojām parādās publicētos fragmentu krājumos. for all ir derīgs un piemēro vienu using un vienu with check visām komandām, ko reti vēlas. Arī insert politika ar using neizdodas: only WITH CHECK expression allowed for INSERT.

Vai politikas palēnina vaicājumus?

Var palēnināt. Politikas izteiksme tiek izpildīta katrai rindai, ko Postgres izskata. Nomērīju select * from big kā lietotājs A 200 000 rindās, kas pieder 1 000 lietotājiem, A pieder 200 (7 palaišanu mediāna, Postgres 17.10):

Viena īpašnieka politika, četri pieraksti. Milisekundes, lai nolasītu 200 savas rindas no 200 000. 7 palaišanu mediāna. auth.uid() = user_id, bez indeksa: 156,30; (select auth.uid()), bez indeksa: 7,40; auth.uid() = user_id, ar indeksu: 0,21; (select auth.uid()), ar indeksu: 0,05.
2. attēls. Mans tests lokālā Postgres 17.10 ar no jauna izveidotām Supabase lomām, 2026. gada 24. oktobrī.
Politika un indekss Mediāna
auth.uid() = user_id, bez indeksa 156,3 ms
(select auth.uid()) = user_id, bez indeksa 7,4 ms
auth.uid() = user_id, indekss uz user_id 0,21 ms
(select auth.uid()) = user_id, indekss 0,05 ms

Izsaukuma ietīšana select ļauj Postgres to aprēķināt vienreiz vaicājumā, nevis vienreiz katrai rindai; pārējo izdara indekss. Supabase RLS veiktspējas ceļvedis savā 100 000 rindu tabulā min tādu pašu izmaiņu kārtu (no 179 ms uz 9 ms). Daudzi bloga ieraksti šos skaitļus citē kā savus; augstāk minētie ir no mana palaiduma.

No kurienes jānāk lomām?

No app_metadata vai jūsu pašu tabulas, nekad no user_metadata. Pierakstījies lietotājs var mainīt savu user_metadata ar supabase.auth.updateUser(), tāpēc politika, kas pārbauda auth.jwt() -> 'user_metadata' ->> 'role', ļauj viņam lomu piešķirt pašam sev. Reģistrācijas trigeris, kas kopē lomu no reģistrācijas metadatiem, dara to pašu; šis gadījums aprakstīts rakstā Supabase raw_user_meta_data: kā reģistrācija kļuva par Owner.

Kā pārbaudīt savas politikas?

  • Bezmaksas Supabase drošības pārbaude nolasa jūsu projektu tikai lasīšanas režīmā un atzīmē vienmēr patiesas politikas, rakstīšanas politikas viesiem, politikas, kas uzticas lietotāja metadatiem, un tabulas ar izslēgtu RLS, katrai ar labošanas SQL.
  • Neviens skeneris nezina, kuras rindas kam pieder. Tam palaidiet pieprasījumus kā divi lietotāji, kā to dara šis raksts; pilna metode lietotnei ar lomām ir rakstā kā es testēju Supabase RLS.
  • Ko Supabase aizsargā jūsu vietā un ko atstāj jums, ir rakstā vai Supabase ir drošs?.
  • Tie paši īpašnieka un komandas testi, palaisti arī uz Appwrite, ir rakstā Appwrite vs Supabase drošība: vienādi uzbrukumi abiem.

Ierobežojumi. Viens lokāls steks un viens klēpjdators, izdomāti dati, 2026. gada 24. oktobris. Laiki ir atkarīgi no aparatūras un tabulas formas; attiecības ir svarīgākas par milisekundēm. Storage politikas un Realtime šeit nav aplūkotas.

Neesat pārliecināti, vai jūsu politikas izturēs?

Labprāt kopā ar jums apskatīšu shēmu. Bez pārdošanas sarunas.

Sazināties

© 2026 Denys Zaichenko. Oriģinālais raksts: https://madsalt.tech/lv/blog/supabase-rls-policy-examples/