Ukratko

Najvažnije iz članka

  • RLS omogućuje filtriranje podataka na razini redaka izravno unutar baze podataka.
  • Idealno je rješenje za multi-tenant aplikacije jer sprječava curenje podataka između klijenata.
  • Politike se oslanjaju na USING i WITH CHECK klauzule za kontrolu čitanja i pisanja.
  • Potrebno je koristiti ne-superuser uloge i pravilno indeksirati stupce za optimalne performanse.
  • Integracija se vrši putem SET LOCAL varijabli unutar transakcija u aplikacijskom kodu.
Sadržaj članka
  1. Što je zapravo Row Level Security (RLS)?
  2. Priprema baze i kreiranje tablica
  3. Definiranje sigurnosnih politika (Policies)
  4. Razlika između USING i WITH CHECK klauzula
  5. Integracija s aplikacijskim slojem i performanse
  6. Napredne tehnike: Role-based RLS
  7. Sigurnosna upozorenja i najbolja praksa
  8. Zaključak

Sigurnost podataka u modernim web aplikacijama često se rješava isključivo na razini aplikacijskog koda (npr. unutar ORM upita). Međutim, takav pristup nosi rizik da programerski propust dovede do curenja osjetljivih informacija jednog klijenta drugome. PostgreSQL Row Level Security (RLS) predstavlja snažan mehanizam koji omogućuje definiranje sigurnosnih pravila izravno na razini baze podataka, čime se osigurava da svaki korisnik vidi isključivo one retke u tablici na koje ima pravo, bez obzira na to kako je SQL upit napisan.

Kada govorimo o multi-tenant arhitekturi (gdje više klijenata dijeli istu bazu podataka), RLS postaje neprocjenjiv alat. Umjesto kreiranja posebne baze ili sheme za svakog klijenta, možemo koristiti jednu zajedničku tablicu, a PostgreSQL će se pobrinuti za izolaciju. U ovom tutorialu proći ćemo kroz cijeli proces: od teoretskih osnova i postavljanja okruženja do kompleksnih politika koje razlikuju administratore od običnih korisnika.

Što je zapravo Row Level Security (RLS)?

Standardni SQL modeli dopuštenja (GRANT, REVOKE) rade na razini cijele tablice, pogleda (view) ili stupca. To znači da ako korisnik ima dopuštenje za SELECT nad tablicom dokumenti, on može vidjeti sve zapise u toj tablici. RLS mijenja tu paradigmu tako što dodaje kontrolu nad svakim pojedinačnim retkom. Kada je RLS omogućen, svaki upit koji korisnik izvrši biva automatski modificiran od strane PostgreSQL planera (query planner) kako bi uključio dodatne uvjete definirane u sigurnosnim politikama.

Djelovanje RLS-a je transparentno za aplikaciju. Ako aplikacija pošalje upit SELECT * FROM narudzbe, PostgreSQL će internim mehanizmom to pretvoriti u nešto poput SELECT * FROM narudzbe WHERE tenant_id = 'moj_id'. Na taj način eliminiramo ljudski faktor u pisanju SQL koda i smanjujemo površinu za napad. Ovo je posebno kritično u SaaS (Software as a Service) rješenjima gdje je izolacija klijenata najviši prioritet.

Priprema baze i kreiranje tablica

Za početak, moramo postaviti osnovnu strukturu. Pretpostavimo da gradimo sustav za upravljanje projektima gdje svaki klijent (tenant) ima svoje korisnike i projekte. Prvi korak je kreiranje tablice za klijente i tablice za korisnike koji će imati pristup tim klijentima. Važno je napomenuti da ćemo koristiti PostgreSQL ulogu (role) koja nije superuser, jer superuseri po defaultu zaobilaze RLS provjere.

CREATE TABLE klijenti (
    id SERIAL PRIMARY KEY,
    naziv TEXT NOT NULL
);

CREATE TABLE projekti (
    id SERIAL PRIMARY KEY,
    klijent_id INTEGER REFERENCES klijenti(id),
    naziv_projekta TEXT NOT NULL,
    tajni_podaci TEXT
);

-- Uključivanje RLS-a na tablici projekti
ALTER TABLE projekti ENABLE ROW LEVEL SECURITY;

Nakon što smo izvršili ALTER TABLE ... ENABLE ROW LEVEL SECURITY, tablica projekti postaje nevidljiva za sve korisnike osim vlasnika tablice ili superusera. Ako pokušate izvršiti SELECT kao običan korisnik bez definiranih politika, dobit ćete prazan skup rezultata. To je 'fail-safe' mehanizam koji osigurava da podaci ne procure slučajno.

Definiranje sigurnosnih politika (Policies)

Sada kada je RLS omogućen, moramo definirati pravila o tome tko može vidjeti koje podatke. U multi-tenant sustavu, najčešće ćemo koristiti varijablu sesije ili specifičnu ulogu kako bismo identificirali trenutnog klijenta. Najjednostavnija politika koristi CURRENT_USER ili prilagođenu varijablu unutar tekuće transakcije.

CREATE POLICY klijent_izolacija_policy ON projekti
    USING (klijent_id = (current_setting('app.current_klijent_id'))::integer);

U ovom primjeru koristimo current_setting, što nam omogućuje da aplikacija pri svakom povezivanju na bazu postavi kontekst: SET app.current_klijent_id = '5';. Svaki idući upit unutar te sesije bit će filtriran tako da se vide samo projekti koji pripadaju klijentu s ID-om 5. Ovo je izuzetno moćno jer omogućuje korištenje istih database connection poolova za različite klijente, uz uvjet da aplikacija ispravno postavi varijablu sesije na početku transakcije.

Razlika između USING i WITH CHECK klauzula

Prilikom definiranja politika, susrećemo se s dvije ključne klauzule: USING i WITH CHECK. Razumijevanje njihove razlike je ključno za stabilnost sustava. USING klauzula se primjenjuje na retke koji se čitaju iz tablice (SELECT, UPDATE, DELETE). Ako redak ne zadovoljava uvjet, on jednostavno nije vidljiv ili dostupan za izmjenu.

S druge strane, WITH CHECK se primjenjuje kod pokušaja unosa novih podataka (INSERT) ili ažuriranja postojećih (UPDATE). Ona sprječava korisnika da npr. kreira projekt koji pripada drugom klijentu.

CREATE POLICY klijent_pisanje_policy ON projekti
    FOR ALL
    USING (klijent_id = (current_setting('app.current_klijent_id'))::integer)
    WITH CHECK (klijent_id = (current_setting('app.current_klijent_id'))::integer);

Bez WITH CHECK dijela, korisnik bi mogao vidjeti samo svoje projekte, ali bi potencijalno mogao izvršiti INSERT s tuđim klijent_id, čime bi efektivno 'ubacio' podatke u tuđi prostor, što bi kasnije rezultiralo time da ih on sam više ne vidi, ali je narušio integritet podataka.

Integracija s aplikacijskim slojem i performanse

Jedno od najčešćih pitanja je: utječe li RLS na performanse? Odgovor je da, ali u većini slučajeva zanemarivo. Budući da PostgreSQL planer ugrađuje RLS uvjete izravno u plan upita, baza može koristiti indekse na isti način kao da ste ručno dodali WHERE klauzulu. Ključno je imati indekse na stupcima koji se koriste u politikama (u našem slučaju klijent_id).

Prilikom integracije s jezicima poput Python (Django/FastAPI), Node.js ili Go, preporučuje se korištenje middleware-a koji će nakon preuzimanja konekcije iz poola izvršiti SET LOCAL app.current_klijent_id = .... Korištenje SET LOCAL unutar transakcije osigurava da se postavka poništi čim transakcija završi, što je kritično za sigurnost u okruženjima s dijeljenim konekcijama.

Napredne tehnike: Role-based RLS

U kompleksnijim sustavima, želimo da se prava razlikuju i unutar istog klijenta (npr. 'admin' može vidjeti sve, 'user' samo svoje zadatke). PostgreSQL RLS omogućuje kombiniranje više politika pomoću OR logike. Možemo definirati politiku za admine i politiku za obične korisnike, a PostgreSQL će dopustiti pristup ako bilo koja od njih vrati true.

CREATE POLICY admin_izolacija ON projekti
    FOR SELECT
    USING (current_user = 'super_admin');

CREATE POLICY user_izolacija ON projekti
    FOR SELECT
    USING (kreator_id = (current_setting('app.user_id'))::integer);

Ovdje vidimo kako RLS može biti modularan. Sustav će dozvoliti SELECT ako je korisnik globalni admin ILI ako je korisnik kreator specifičnog projekta. Ovakva fleksibilnost omogućuje implementaciju kompleksnih RBAC (Role-Based Access Control) ili ABAC (Attribute-Based Access Control) sustava bez kompliciranja aplikacijske logike.

Sigurnosna upozorenja i najbolja praksa

Iako je RLS izuzetno moćan, on nije čarobno rješenje. Postoje određene zamke na koje developeri moraju paziti. Prva je spomenuta uloga superusera. Superuseri uvijek zaobilaze RLS. Također, tablice koje su u vlasništvu određene uloge neće primjenjivati RLS za tu ulogu, osim ako se izričito ne navede FORCE ROW LEVEL SECURITY.

Druga bitna stvar su funkcije koje se izvršavaju s SECURITY DEFINER privilegijama. Takve funkcije se izvršavaju s pravima korisnika koji je kreirao funkciju, a ne onoga tko ju poziva, što može nenamjerno zaobići RLS. Uvijek je preporuka koristiti SECURITY INVOKER za funkcije koje trebaju poštovati RLS pravila.

Konačno, testiranje je ključno. Sigurnosne politike treba testirati automatiziranim testovima koji simuliraju upite iz perspektive različitih korisnika i klijenata kako bi se osiguralo da nema propusta (tzv. cross-tenant data leakage).

Zaključak

PostgreSQL Row Level Security pretvara bazu podataka iz pasivnog skladišta u aktivnog čuvara integriteta i privatnosti podataka. Za multi-tenant aplikacije, RLS nudi elegantno rješenje koje smanjuje kompleksnost aplikacijskog koda i pruža snažnu garanciju da su podaci klijenta izolirani na najnižoj mogućoj razini. Iako zahtijeva pažljivo planiranje indeksa i rukovanje sesijama unutar transakcija, dugoročna korist u sigurnosti i održavanju sustava daleko nadmašuje inicijalni trud u postavljanju.

Izvori i dodatno čitanje

  1. PostgreSQL Documentation: Row Security Policies
  2. Ars Technica: Database Security Best Practices
  3. Bug.hr: Razvoj modernih web aplikacija
B
Uredništvo portala

BAJT

Službeni autorski profil redakcije portala BAJT. Sadržaj priprema i provjerava uredništvo portala.