IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis9/65-dars23 daqiqa
Mundarija (37)

PostgreSQL Row Level Security: CREATE POLICY, SET LOCAL va pool tuzoqlari

Qisqacha: Row Level Security (RLS) — har so'rovga bazaning o'zi qo'shadigan majburiy filtr: "bu rol faqat egasi_id = joriy foydalanuvchi bo'lgan qatorlarni ko'radi". Jadvalda ENABLE ROW LEVEL SECURITY, keyin CREATE POLICY … USING (…) WITH CHECK (…). Joriy foydalanuvchini ilova har tranzaksiyada SET LOCAL (yoki set_config(…, true)) bilan beradi. Ilova WHERE ni unutsa ham begona qator chiqmaydi. Lekin superuser va jadval egasi RLS'ni chetlab o'tadi, SET (LOCAL'siz) esa pool'da keyingi foydalanuvchiga sizadi.

Bu darsda

  • ENABLE ROW LEVEL SECURITY va CREATE POLICY bilan qatorlar darajasidagi himoyani yoqasiz.
  • USING va WITH CHECK farqini, nega begona qatorni UPDATE qilish xato emas, 0 berishini tushuntirasiz.
  • Joriy foydalanuvchini SET LOCAL / set_config bilan xavfsiz uzatasiz va bo'sh satr tuzog'ini bilasiz.
  • Superuser, jadval egasi, FORCE va BYPASSRLS qoidalarini ajratasiz.
  • RLS narxini o'lchaysiz va subquery'li siyosat qachon qimmat ekanini bilasiz.

Oldin bilishingiz kerak: Funksiyalar va PL/pgSQL, Tranzaksiya asoslari, Connection pooling, PgBouncer va tashqi pooler'lar.

1. Nega bu kerak?

vazifalar-api dagi har so'rovda bir qator bor: WHERE egasi_id = $1. Uni yuzlab marta yozamiz — ro'yxat, bitta vazifa, tahrir, o'chirish, statistika, eksport. Bitta joyda unutildi — va Aziz Dilnozaning vazifalarini ko'radi. Bu xato turi shunchalik ko'p uchraydiki, OWASP ro'yxatida birinchi o'rinda turadi: Broken Access Control (buzilgan kirish nazorati).

RLS — ikkinchi himoya qatlami. Ilova WHERE ni yozadi (birinchi qatlam), baza esa baribir o'zining filtrini qo'shadi. Ilova xato qilsa ham ma'lumot sizmaydi.

O'xshatish: bank seyfxonasi. Kassir sizni seyfingizga olib boradi (ilova). Lekin har seyfning o'z kaliti bor (RLS): kassir adashib boshqa eshikka olib borsa ham, kalitingiz u eshikni ochmaydi.

Bu darsdagi so'rovlarni biz ishga tushirdik: PostgreSQL 17.6, kanon holat 09 sxemasi (105-rls.sql qo'llangan). Kanon (IlmHamroh, 2026-10, PostgreSQL 18.6) bilan chiqish to'liq bir xil.

2. Birinchi siyosat

2.1 Ikki qadam: yoqish va siyosat

Kichik misoldan boshlaymiz. Jadvalda RLS yoqilgan, lekin hech qanday siyosat yo'q. Ilova roli nimani ko'radi? (vazifalar_ilova — kanon yaratgan ilova roli; rollar haqida to'liq — Rollar va huquqlar darsida. SET LOCAL ROLE — tranzaksiya oxirigacha shu rol bo'lib ishlash.)

sql
BEGIN;
CREATE TABLE m09_eslatma (egasi bigint, matn text);
INSERT INTO m09_eslatma VALUES (1, 'Aziz eslatmasi'),
  (2, 'Dilnoza eslatmasi');
GRANT SELECT ON m09_eslatma TO vazifalar_ilova;
ALTER TABLE m09_eslatma ENABLE ROW LEVEL SECURITY;
SET LOCAL ROLE vazifalar_ilova;
SELECT count(*) FROM m09_eslatma;
ROLLBACK;
text
BEGIN
CREATE TABLE
INSERT 0 2
GRANT
ALTER TABLE
SET
 count
-------
     0
(1 row)

ROLLBACK

GRANT SELECT bor, lekin 0 qator. Qoida: RLS yoqilgan va mos siyosat yo'q — hech narsa ko'rinmaydi (default deny). Xato emas, bo'sh natija. Bu "yopiq holatda xavfsiz" (fail closed) tamoyili: unutilgan siyosat ma'lumotni ochmaydi, yopadi.

Demak RLS uchun ikki narsa kerak: GRANT (jadvalga umuman kira oladimi) va siyosat (qaysi qatorlarga). Biri ikkinchisining o'rnini bosmaydi.

2.2 Kanon siyosatlari

Kanonning 105-rls.sql migratsiyasi besh jadvalda RLS yoqadi. Ro'yxatni katalogdan ko'ramiz:

sql
SELECT tablename, policyname, cmd, roles, qual
FROM pg_policies ORDER BY 1, 2;
text
    tablename     |      policyname      | cmd |       roles       |                    qual
------------------+----------------------+-----+-------------------+---------------------------------------------
 foydalanuvchilar | foydalanuvchilar_ozi | ALL | {vazifalar_ilova} | (id = joriy_foydalanuvchi())
 izohlar          | izohlar_vazifa_egasi | ALL | {vazifalar_ilova} | (EXISTS ( SELECT 1                         +
                  |                      |     |                   |    FROM vazifalar v                        +
                  |                      |     |                   |   WHERE (v.id = izohlar.vazifa_id)))
 teglar           | teglar_egasi         | ALL | {vazifalar_ilova} | (egasi_id = joriy_foydalanuvchi())
 vazifa_teglari   | vazifa_teglari_egasi | ALL | {vazifalar_ilova} | (EXISTS ( SELECT 1                         +
                  |                      |     |                   |    FROM vazifalar v                        +
                  |                      |     |                   |   WHERE (v.id = vazifa_teglari.vazifa_id)))
 vazifalar        | vazifalar_egasi      | ALL | {vazifalar_ilova} | (egasi_id = joriy_foydalanuvchi())
(5 rows)

Ustunlar: cmd — qaysi buyruqlar uchun (ALL — hammasi), roles — kimga, qual — USING sharti. Uchta jadvalda egasi to'g'ridan-to'g'ri (egasi_id = …), ikkitasida — vazifa orqali: "izoh ko'rinadi, agar uning vazifasi ko'rinsa". EXISTS ichidagi vazifalar ham RLS bilan filtrlanadi — himoya zanjir bo'lib ulanadi.

Kanon vazifalar siyosati:

sql
CREATE POLICY vazifalar_egasi ON vazifalar TO vazifalar_ilova
  USING (egasi_id = joriy_foydalanuvchi())
  WITH CHECK (egasi_id = joriy_foydalanuvchi());

3. joriy_foydalanuvchi() qayerdan biladi

3.1 Sozlama — tranzaksiya "yorlig'i"

Baza HTTP so'rovni ko'rmaydi — "kim kirgan" ni ilova aytishi kerak. Kanon Soft delete, timestamps va tarix jadvallari darsidagi usulni ishlatadi: o'zimiz nomlagan sozlama (ilova.foydalanuvchi_id) va uni o'qiydigan funksiya:

sql
CREATE FUNCTION joriy_foydalanuvchi() RETURNS bigint
LANGUAGE sql STABLE PARALLEL SAFE
RETURN nullif(
  current_setting('ilova.foydalanuvchi_id', true), '')::bigint;
  • current_setting(nom, true) — sozlamani o'qiydi; true — "yo'q bo'lsa xato berma, NULL qaytar".
  • nullif(…, '') — bo'sh satrni NULL ga aylantiradi (nega — pastda).
  • STABLE — bitta so'rov ichida o'zgarmaydi, shuning uchun indeks shartiga tushadi (Funksiyalar va PL/pgSQL darsidagi volatility).

Xaritadagi umumiy nom app.tenant_id — ko'p ijarachili (multi-tenant) tizimlarda "qaysi kompaniya" ni bildiradi. Bizda ijarachi — foydalanuvchi. Mexanizm bir xil.

3.2 Superuser, ilova roli, SET LOCAL

Uch holat. Avval superuser:

sql
SELECT current_user,
       (SELECT rolbypassrls FROM pg_roles
        WHERE rolname = current_user) AS bypass,
       count(*) FROM vazifalar;
text
 current_user | bypass | count
--------------+--------+-------
 postgres     | t      |    48
(1 row)

postgres — superuser, RLS'ni chetlab o'tadi (bypass t): 48 ta, hammaniki. Endi ilova roli — avval foydalanuvchisiz, keyin SET LOCAL bilan:

sql
SET ROLE vazifalar_ilova;
SELECT current_user, joriy_foydalanuvchi(), count(*) FROM vazifalar;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
SELECT joriy_foydalanuvchi(), count(*) AS vazifalar,
       (SELECT count(*) FROM izohlar) AS izohlar,
       (SELECT count(*) FROM teglar) AS teglar,
       (SELECT count(*) FROM foydalanuvchilar) AS foydalanuvchilar
FROM vazifalar;
SELECT DISTINCT egasi_id FROM vazifalar;
ROLLBACK;
text
SET
  current_user   | joriy_foydalanuvchi | count
-----------------+---------------------+-------
 vazifalar_ilova |                     |     0
(1 row)

BEGIN
SET
 joriy_foydalanuvchi | vazifalar | izohlar | teglar | foydalanuvchilar
---------------------+-----------+---------+--------+------------------
                   1 |        13 |       6 |      8 |                1
(1 row)

 egasi_id
----------
        1
(1 row)

ROLLBACK
  • Foydalanuvchi o'rnatilmagan — NULL, 0 qator. Yana "yopiq holatda xavfsiz": egasi_id = NULL hech qachon true emas (NULL mantiqi).
  • SET LOCAL … = '1' — Aziz: 13 vazifa (12 faol + 1 Savatda), 6 izoh, 8 teg va faqat o'zi foydalanuvchilar jadvalida.
  • SELECT DISTINCT egasi_id — WHERE yo'q, lekin faqat 1. Ilova "unutgan" filtrni baza qo'ydi.

3.3 Ilova kodidan: set_config

SET LOCAL ga qiymatni parametr bilan berib bo'lmaydi — SET buyrug'i $1 ni qabul qilmaydi. psql 16+ dagi \bind (parametrli so'rov — Node'dagi client.query(sql, [qiymat]) ning o'zi) bilan sinaymiz:

sql
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = $1 \bind '1' \g
ROLLBACK;
BEGIN;
SELECT set_config('ilova.foydalanuvchi_id', $1, true) \bind '1' \g
ROLLBACK;
text
BEGIN
ERROR:  syntax error at or near "$1"
LINE 1: SET LOCAL ilova.foydalanuvchi_id = $1
                                           ^
ROLLBACK
BEGIN
 set_config
------------
 1
(1 row)

ROLLBACK

set_config(nom, qiymat, true) — SET LOCAL ning funksiya shakli (true — "faqat shu tranzaksiyaga"). U parametr qabul qiladi. Bu muhim: aks holda dasturchi SET LOCAL … = '${id}' deb satr ulaydi — va SQL injection eshigini ochadi.

Ilovadagi naqsh shunday ko'rinadi (namuna, pg bilan; kanonning vazifalar-api sida hali RLS ulanmagan — pastda «Loyiha qadami» da):

js
async function foydalanuvchiNomidan(pool, foydalanuvchiId, ish) {
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    await client.query(
      "SELECT set_config('ilova.foydalanuvchi_id', $1, true)",
      [String(foydalanuvchiId)]);
    const natija = await ish(client);
    await client.query("COMMIT");
    return natija;
  } catch (xato) {
    await client.query("ROLLBACK");
    throw xato;
  } finally {
    client.release();
  }
}

Diagrammada bitta HTTP so'rovning yo'li — set_config va so'rov bitta tranzaksiyada, bitta ulanishda:

sequenceDiagram
  participant A as API
  participant P as Pool
  participant B as Postgres
  A->>P: connect()
  P-->>A: ulanish
  A->>B: BEGIN
  A->>B: set_config(id, 1, true)
  A->>B: SELECT * FROM vazifalar
  B-->>A: faqat egasi 1 qatorlari
  A->>B: COMMIT (sozlama tozalanadi)
  A->>P: release()

Tekshirib ko'ring: Nega set_config alohida pool.query() bilan, asosiy so'rov esa boshqa pool.query() bilan bajarilsa, RLS ishlamaydi?

Javob

pool.query() har safar pool'dan istalgan bo'sh ulanishni oladi — ikki chaqiruv ikki boshqa ulanishga tushishi mumkin (Tranzaksiya asoslari darsidagi pool.query('BEGIN') tuzog'i). Bundan tashqari, tranzaksiyasiz set_config(…, true) faqat shu bitta buyruq tranzaksiyasida yashaydi. Natija: asosiy so'rov foydalanuvchisiz bajariladi — 0 qator. Shuning uchun bitta client, BEGIN … COMMIT.

4. USING va WITH CHECK

4.1 Begona qatorni o'zgartirish — 0, xato emas

USING — qaysi mavjud qatorlar ko'rinadi (SELECT, UPDATE, DELETE uchun). Ko'rinmaydigan qator — yo'qdek:

sql
SET ROLE vazifalar_ilova;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
UPDATE vazifalar SET matn = 'buzildi' WHERE id = 3;
DELETE FROM vazifalar WHERE egasi_id = 2;
ROLLBACK;
text
SET
BEGIN
SET
UPDATE 0
DELETE 0
ROLLBACK

3-vazifa Dilnozaniki. Aziz uni o'zgartira olmadi — lekin xato ham olmadi: UPDATE 0. Bu ataylab: "bunday qator bor, lekin sizniki emas" deyish — ma'lumot sizishi. Ilova 0 ni "topilmadi" (404) deb talqin qiladi — UPDATE va DELETE xavfsiz darsidagi rowCount tekshiruvi.

4.2 WITH CHECK — yangi qator uchun

WITH CHECK — yozilayotgan qator (INSERT va UPDATE natijasi) shartga mos bo'lishi kerak:

sql
SET ROLE vazifalar_ilova;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
INSERT INTO vazifalar (egasi_id, matn)
VALUES (2, 'Dilnozaga yashirin vazifa');
ROLLBACK;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
UPDATE vazifalar SET egasi_id = 2 WHERE id = 1;
ROLLBACK;
text
SET
BEGIN
SET
ERROR:  new row violates row-level security policy for table "vazifalar"
ROLLBACK
BEGIN
SET
ERROR:  new row violates row-level security policy for table "vazifalar"
ROLLBACK

Tarjimasi: "yangi qator vazifalar jadvalining qator darajasidagi xavfsizlik siyosatini buzadi". Ikki hujum to'xtadi: begona nomiga yozish va o'z vazifasini begona nomiga "ko'chirish". WITH CHECK siz ikkinchisi o'tardi — USING faqat eski qatorga qaraydi (1-vazifa Azizniki edi).

4.3 Trigger'lar RLS ostida

O'z nomiga yozish — ishlaydi, trigger'lar ham:

sql
SET ROLE vazifalar_ilova;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
INSERT INTO vazifalar (egasi_id, matn)
VALUES (1, 'RLS ostida yangi vazifa #rls')
RETURNING id, tartib, versiya;
INSERT INTO izohlar (vazifa_id, muallif_id, matn)
VALUES (1, 1, 'O''z vazifamga izoh') RETURNING id;
INSERT INTO izohlar (vazifa_id, muallif_id, matn)
VALUES (3, 1, 'Begona vazifaga izoh');
ROLLBACK;
text
SET
BEGIN
SET
 id | tartib | versiya
----+--------+---------
 50 |     14 |       1
(1 row)

INSERT 0 1
 id
----
 16
(1 row)

INSERT 0 1
ERROR:  new row violates row-level security policy for table "izohlar"
ROLLBACK

tartib = 14 — Triggerlar darsidagi tartibni_qoy RLS ostida max(tartib) ni hisobladi va faqat Azizning vazifalarini ko'rdi (13 ta → 14). Bu to'g'ri: tartib har egasi uchun alohida. Begona vazifaga izoh — rad: izohlar siyosatidagi EXISTS (… vazifalar …) 3-vazifani ko'rmadi.

Diqqat: Trigger'lar RLS ostida ishlaydi — bu ba'zan kutilmagan natija beradi. Masalan, "butun tizimdagi vazifalar sonini" hisoblaydigan trigger ilova roli bilan faqat bitta foydalanuvchinikini sanaydi. Bunday trigger funksiyasi SECURITY DEFINER bo'lishi kerak (egasining huquqi bilan, Funksiyalar va PL/pgSQL darsidagi himoyalar bilan).

4.4 Bir nechta siyosat: PERMISSIVE va RESTRICTIVE

Bir jadvalda bir nechta siyosat bo'lsa, sukut bo'yicha ular OR bilan birlashadi (PERMISSIVE — "biri ruxsat bersa yetadi"). AS RESTRICTIVE siyosat esa AND bilan qo'shiladi — "baribir shu shart ham bajarilsin". Misol: ilova roli Savatdagi vazifalarni umuman ko'rmasin:

sql
BEGIN;
CREATE POLICY m09_faqat_faol ON vazifalar AS RESTRICTIVE
  TO vazifalar_ilova USING (ochirilgan_vaqt IS NULL);
SET LOCAL ROLE vazifalar_ilova;
SET LOCAL ilova.foydalanuvchi_id = '1';
SELECT count(*) FROM vazifalar;
ROLLBACK;
text
BEGIN
CREATE POLICY
SET
SET
 count
-------
    12
(1 row)

ROLLBACK

13 emas, 12: egasi sharti va "o'chirilmagan" sharti. Ehtiyot bo'ling: faqat RESTRICTIVE siyosatlar bo'lib, birorta PERMISSIVE bo'lmasa — hech narsa ko'rinmaydi. FOR SELECT, FOR INSERT, FOR UPDATE, FOR DELETE bilan siyosatni buyruq bo'yicha ham bo'lish mumkin (masalan, o'qish keng, yozish tor).

5. Tuzoqlar: bo'sh satr va pool

5.1 Bo'sh satr tuzog'i

SET LOCAL tugagach sozlama yo'qolmaydi — u bo'sh satr bo'lib qoladi:

sql
SET ROLE vazifalar_ilova;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
COMMIT;
SELECT current_setting('ilova.foydalanuvchi_id', true) = ''
         AS bosh_satr,
       joriy_foydalanuvchi();
SELECT current_setting('ilova.foydalanuvchi_id', true)::bigint;
text
SET
BEGIN
SET
COMMIT
 bosh_satr | joriy_foydalanuvchi
-----------+---------------------
 t         |
(1 row)

ERROR:  invalid input syntax for type bigint: ""

''::bigint — xato. Agar siyosat nullif siz yozilganida, ulanishdagi ikkinchi tranzaksiyadan boshlab har so'rov shu xato bilan yiqilardi — birinchisi esa ishlardi. Bunday "ba'zan ishlaydi" xatoni topish juda qiyin. nullif(…, '') — kanon buni o'lchab topgan va funksiyaga qo'shgan.

5.2 SET (LOCAL'siz) — keyingi mijozga sizadi

Endi eng xavfli tuzoq. Dasturchi LOCAL ni unutdi:

sql
SET ROLE vazifalar_ilova;
SET ilova.foydalanuvchi_id = '1';
SELECT count(*) AS keyingi_sorov_korgani FROM vazifalar;
RESET ilova.foydalanuvchi_id;
RESET ROLE;
text
SET
SET
 keyingi_sorov_korgani
-----------------------
                    13
(1 row)

RESET
RESET

SET — sessiya sozlamasi: ulanish yopilguncha qoladi. Pool'da ulanish yopilmaydi — keyingi HTTP so'rovga (boshqa foydalanuvchiga!) beriladi. Agar keyingi so'rov o'z set_config ini qilmasa (masalan, xato yo'lda yoki "public" endpoint'da), u Azizning 13 ta vazifasini ko'radi. RLS ma'lumotni himoya qilish o'rniga, uni boshqa odamga uzatdi.

Pool nuqtai nazaridan bir ulanishning hayoti (3-mashqda shuni o'zingiz takrorlaysiz):

Qadam Ulanishdagi so'rov Sessiya sozlamasi Natija
1 Aziz: BEGIN, set_config(…, '1', true), SELECT, COMMIT COMMIT dan keyin '' Aziz o'z 13 vazifasini ko'rdi
2 Aziz: SET ilova.foydalanuvchi_id = '1' (LOCAL'siz) '1' — ulanishda qoladi Aziz o'z vazifalarini ko'rdi
3 Ulanish pool'ga qaytdi '1' —
4 Dilnoza: set_config unutilgan, SELECT hali '1' Dilnoza Azizning 13 vazifasini ko'rdi

Qoidalar:

  • Faqat SET LOCAL / set_config(…, true), faqat BEGIN … COMMIT ichida.
  • PgBouncer transaction rejimida SET (sessiya) umuman ishonchsiz — ulanishlar tranzaksiya oxirida almashadi (PgBouncer va tashqi pooler'lar); SET LOCAL esa tranzaksiya ichida qolgani uchun to'g'ri ishlaydi.
  • Pool'ga qaytarishda DISCARD ALL / RESET ALL — qo'shimcha himoya.

Tekshirib ko'ring: API'da "admin hamma vazifani ko'radi" degan endpoint kerak. Admin uchun set_config ga nima beriladi?

Javob

Hech narsa — bu boshqa vazifa. Adminga "maxsus id" (masalan 0 yoki -1) berib siyosatga OR joriy_foydalanuvchi() = -1 qo'shish — xavfli: shu qiymat qayerdadir sizsa, hamma narsa ochiladi. To'g'ri yo'llar: admin uchun alohida rol (alohida siyosat yoki BYPASSRLS — keyingi bo'lim) yoki admin so'rovlarini alohida ulanish/xizmat orqali bajarish. Kanonda shunday: hisobot roli BYPASSRLS bilan (Rollar va huquqlar).

6. Kim chetlab o'tadi: superuser, egasi, FORCE, BYPASSRLS

Kim RLS ishlaydimi
superuser (postgres) yo'q — hech qachon
BYPASSRLS atributli rol yo'q
jadval egasi sukut bo'yicha yo'q; FORCE ROW LEVEL SECURITY bo'lsa — ha
boshqa rollar (vazifalar_ilova) ha

Kanon tekshirgan: FORCE superuser'ga ta'sir qilmaydi:

sql
ALTER TABLE teglar FORCE ROW LEVEL SECURITY;
SELECT count(*) AS superuser_force_bilan FROM teglar;
ALTER TABLE teglar NO FORCE ROW LEVEL SECURITY;
text
ALTER TABLE
 superuser_force_bilan
-----------------------
                    32
(1 row)

ALTER TABLE

32 ta — hammasi. Amaliy xulosa: ilova superuser bilan ulansa, RLS umuman ishlamaydi. Ko'p jamoalar RLS siyosatlarini yozib, ilovani postgres bilan ulab qo'yadi va himoya bor deb o'ylaydi. Ilova — superuser emas, jadval egasi ham emas, cheklangan rol (Rollar va huquqlar darsida shu tizimni quramiz).

7. Hujumchi nigohi

Hujumchi Azizning hisobiga kirdi va API'ni sinaydi:

  1. GET /v1/vazifalar/3 (begona id) — ilova WHERE egasi_id ni unutgan endpoint. RLS: 0 qator → 404. Sizish yo'q.
  2. PATCH /v1/vazifalar/1 {"egasiId": 2} — o'z vazifasini begonaga "tashlash" (spam). WITH CHECK → xato.
  3. POST /v1/vazifalar/3/izohlar — begona vazifaga izoh. Izohlar siyosatidagi EXISTS → xato.
  4. Vaqt hujumi: so'rov "bor, lekin ruxsat yo'q" va "yo'q" holatda bir xil javob beradimi? RLS'da ikkalasi ham 0 qator — farq yo'q.

RLS himoya qilmaydigan narsalar: ilova o'zi noto'g'ri set_config qilsa (masalan, foydalanuvchi id'sini so'rov tanasidan olsa — tokendan emas); ilova superuser bilan ulansa; SQL injection orqali hujumchi set_config ni o'zi chaqira olsa. Shuning uchun RLS — qo'shimcha qatlam, WHERE va parametrli so'rovlarning o'rnini bosmaydi.

Yana bir nozik joy: siyosat ichidagi funksiya xato bilan ma'lumot sizdirishi mumkin. View va materialized view darsidagi security_barrier misolini eslang — arzon funksiya NOTICE bilan begona qatorlarni ko'rsatgan edi. RLS siyosatlari avtomatik "barrier" kabi ishlaydi: foydalanuvchi shartlari siyosatdan keyin bajariladi, faqat LEAKPROOF deb belgilangan funksiyalar bundan mustasno.

8. Narx

8.1 Oddiy siyosat — deyarli bepul

Siyosat sharti so'rovga qo'shiladi va indeksdan foydalanadi. Kanon katta to'plamda (500 000 vazifa, egasi 77) o'lchagan: ro'yxat (50 ta, tartib indeksi) superuser + aniq WHERE bilan 0.06–0.09 ms, ilova roli + RLS bilan 0.06–0.08 ms. Farq yo'q — siyosat Index Cond ga aylandi.

8.2 Subquery'li siyosat — qimmat bo'lishi mumkin

izohlar siyosati EXISTS (SELECT … FROM vazifalar …) — har izoh uchun "vazifasi ko'rinadimi?". Kichik holatda reja:

sql
SET ROLE vazifalar_ilova;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
EXPLAIN (COSTS OFF) SELECT i.id FROM izohlar i;
COMMIT;
text
SET
BEGIN
SET
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Seq Scan on izohlar i
   Filter: (ANY (vazifa_id = (hashed SubPlan 2).col1))
   SubPlan 2
     ->  Seq Scan on vazifalar v
           Filter: (egasi_id = (NULLIF(current_setting('ilova.foydalanuvchi_id'::text, true), ''::text))::bigint)
(5 rows)

COMMIT

Postgres foydalanuvchining vazifa id'larini bir marta yig'adi (hashed SubPlan), keyin butun izohlar jadvalini o'qib, har izohni shu ro'yxat bilan tekshiradi. Kanon katta to'plamda SELECT count(*) FROM izohlar:

Foydalanuvchining izohlari soni: 125 000 izoh ichidan (ms)
  • RLS: EXISTS siyosati79–132 ms, Seq Scan 125 000 izoh132 ms
  • Aniq JOIN + WHERE1.0–1.2 ms, indeks1,2 ms

Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6 (Docker), 500 000 vazifa, 125 000 izoh, egasi 77

100 barobardan ortiq farq. Saboq kanon xulosasi bilan: RLS'ni WHERE o'rnida emas, qo'shimcha himoya deb ishlating. Ilova baribir aniq so'rovni yozadi (JOIN vazifalar … WHERE egasi_id = $1) — planner tez yo'lni topadi, siyosat esa xato bo'lsa sug'urta. Subquery'li siyosatlar narxini katta ma'lumotda o'lchang; kerak bo'lsa izohlarga egasi_id ustunini qo'shib (denormalizatsiya), siyosatni oddiy shartga aylantiring.

9. Ko'p uchraydigan xatolar

9.1 Ilova superuser bilan ulangan

Siyosatlar bor, lekin hech narsa filtrlanmaydi. Ilova — cheklangan rol, BYPASSRLS siz.

9.2 SET o'rniga SET LOCAL

Pool'da sozlama keyingi foydalanuvchiga sizadi. set_config(…, true) faqat tranzaksiya ichida.

9.3 SET LOCAL … = '${id}' — satr ulash

SQL injection. set_config('…', $1, true) — parametr bilan.

9.4 WITH CHECK siz siyosat

O'z qatorini begonaga "ko'chirish" mumkin. Yozish mumkin bo'lgan jadvalda WITH CHECK (yoki USING ning o'zi WITH CHECK sifatida — ALL siyosatda WITH CHECK yozilmasa, USING ishlatiladi; lekin aniq yozish o'qishni osonlashtiradi).

9.5 UPDATE 0 ni muvaffaqiyat deb hisoblash

Begona qator — 0, xato emas. Ilova rowCount ni tekshirib, 404 qaytaradi.

9.6 nullif siz current_setting(…)::bigint

Ikkinchi tranzaksiyadan invalid input syntax for type bigint: "". nullif(current_setting(…, true), '').

10. Mashqlar

Mashqlar kurs/mashqlar/27/09-rls/ da. Ular o'z jadvallaringiz va o'z rolingiz bilan, istalgan bo'sh bazada bajariladi; yechimlar tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6). Rol klaster darajasida yaratiladi — oxirida o'chiring.

1-mashq (oson): Default deny

m09_hamyon (egasi int, balans int) jadvaliga ikki qator qo'shing, m09_mijoz rolini yarating va unga SELECT bering. RLS yoqing, siyosatsiz — SET ROLE m09_mijoz bilan nechta qator ko'rinadi? Keyin superuser sifatida-chi?

Yechim
sql
CREATE ROLE m09_mijoz;
CREATE TABLE m09_hamyon (egasi int, balans int);
INSERT INTO m09_hamyon VALUES (1, 50000), (2, 120000);
GRANT SELECT ON m09_hamyon TO m09_mijoz;
ALTER TABLE m09_hamyon ENABLE ROW LEVEL SECURITY;
SET ROLE m09_mijoz;
SELECT 'mijoz: ' || count(*) FROM m09_hamyon;
RESET ROLE;
SELECT 'superuser: ' || count(*) FROM m09_hamyon;
DROP TABLE m09_hamyon;
DROP ROLE m09_mijoz;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
mijoz: 0
superuser: 2

Siyosat yo'q — mijoz hech narsa ko'rmaydi (fail closed); superuser RLS'ni chetlab o'tadi.

2-mashq (o'rta): Hamyon siyosati

1-mashq jadvaliga siyosat qo'shing. m09_mijoz faqat egasi = current_setting('m09.egasi', true)::int bo'lgan qatorni ko'rsin va yangilasin (USING + WITH CHECK). Unga UPDATE huquqini ham bering. egasi = 1 sifatida: (1) o'z balansini o'qing, (2) 2-hamyonga balans = 0 yozishga urining (nechta qator?), (3) o'z qatorining egasi sini 2 ga o'zgartirishga urining. Uchinchi urinish xato beradi — uni alohida tranzaksiyada qiling. nullif ni unutmang.

Yechim
sql
CREATE ROLE m09_mijoz;
CREATE TABLE m09_hamyon (egasi int, balans int);
INSERT INTO m09_hamyon VALUES (1, 50000), (2, 120000);
GRANT SELECT, UPDATE ON m09_hamyon TO m09_mijoz;
ALTER TABLE m09_hamyon ENABLE ROW LEVEL SECURITY;
CREATE POLICY m09_ozi ON m09_hamyon TO m09_mijoz
  USING (egasi = nullif(current_setting('m09.egasi', true), '')::int)
  WITH CHECK
    (egasi = nullif(current_setting('m09.egasi', true), '')::int);
-- xatoni ushlab, matnini qaytaradi (chaqiruvchi huquqi bilan)
CREATE FUNCTION m09_kochir() RETURNS text LANGUAGE plpgsql AS $$
BEGIN
  UPDATE m09_hamyon SET egasi = 2 WHERE egasi = 1;
  RETURN 'o''tdi';
EXCEPTION WHEN insufficient_privilege THEN
  RETURN 'rad: ' || SQLERRM;
END $$;
SET ROLE m09_mijoz;
BEGIN;
SELECT set_config('m09.egasi', '1', true) IS NOT NULL;
SELECT 'balans: ' || balans FROM m09_hamyon;
WITH o AS (UPDATE m09_hamyon SET balans = 0 WHERE egasi = 2
           RETURNING 1)
SELECT 'begona yangilandi: ' || count(*) FROM o;
SELECT m09_kochir();
COMMIT;
RESET ROLE;
SELECT 'Dilnoza: ' || balans FROM m09_hamyon WHERE egasi = 2;
DROP FUNCTION m09_kochir;
DROP TABLE m09_hamyon;
DROP ROLE m09_mijoz;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
t
balans: 50000
begona yangilandi: 0
rad: new row violates row-level security policy for table "m09_hamyon"
Dilnoza: 120000

Begona hamyon 0 qator bilan o'tdi (ko'rinmadi), egasi ni o'zgartirish WITH CHECK dan o'tmadi, Dilnozaning balansi joyida. RLS xatosining kodi 42501 (insufficient_privilege) — m09_kochir shuni ushladi (Funksiyalar va PL/pgSQL darsidagi EXCEPTION). Yordamchi funksiya SECURITY DEFINER emas — chaqiruvchi (mijoz) huquqi bilan ishlaydi, shuning uchun RLS qo'llandi. WITH … RETURNING — o'zgargan qatorlarni sanash uchun (CTE (WITH)).

3-mashq (qiyin): Pool sizishini ko'rsating

2-mashqdagi jadval va siyosat bilan (yangilashsiz, faqat SELECT) bitta sessiyada uch "HTTP so'rov"ni taqlid qiling: (1) Aziz — BEGIN; set_config(…, '1', true); SELECT …; COMMIT;, (2) "xato" so'rov — set_config('m09.egasi', '1', false) (LOCAL'siz!) va SELECT, (3) Dilnozaning so'rovi — dasturchi set_config ni unutgan, faqat SELECT. Har so'rov ko'rgan balansni chiqaring. Uchinchi so'rov nimani ko'rdi va nega?

Yechim
sql
CREATE ROLE m09_mijoz;
CREATE TABLE m09_hamyon (egasi int, balans int);
INSERT INTO m09_hamyon VALUES (1, 50000), (2, 120000);
GRANT SELECT ON m09_hamyon TO m09_mijoz;
ALTER TABLE m09_hamyon ENABLE ROW LEVEL SECURITY;
CREATE POLICY m09_ozi ON m09_hamyon TO m09_mijoz
  USING (egasi = nullif(current_setting('m09.egasi', true), '')::int);
SET ROLE m09_mijoz;
-- 1) Aziz, to'g'ri
BEGIN;
SELECT set_config('m09.egasi', '1', true) IS NOT NULL;
SELECT '1-sorov: ' || coalesce(string_agg(balans::text, ','), '-')
FROM m09_hamyon;
COMMIT;
-- 2) LOCAL'siz — sessiyada qoladi
SELECT set_config('m09.egasi', '1', false) IS NOT NULL;
SELECT '2-sorov: ' || coalesce(string_agg(balans::text, ','), '-')
FROM m09_hamyon;
-- 3) Dilnoza, set_config unutilgan
SELECT '3-sorov: ' || coalesce(string_agg(balans::text, ','), '-')
FROM m09_hamyon;
RESET ROLE;
DROP TABLE m09_hamyon;
DROP ROLE m09_mijoz;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
t
1-sorov: 50000
t
2-sorov: 50000
3-sorov: 50000

Uchinchi so'rov — Dilnoza — Azizning balansini ko'rdi. 2-so'rovdagi sessiya sozlamasi ulanishda qoldi; pool'da bu ulanish keyingi mijozga beriladi. Agar 2-so'rov ham true (LOCAL) bilan bo'lganida, 3-so'rov - (hech narsa) ko'rardi — "yopiq holatda xavfsiz".

11. Loyiha qadami: RLS migratsiyasi (holat 09)

Holat 07 → holat 09. Bitta fayl — sql/105-rls.sql. Kanon fayli to'liq; uzun qatorlar va izohlar telefon uchun bo'lindi, mazmuni aynan (faqat izohlardagi ichki o'lchov fayli nomlari tushirildi):

sql
-- 105-rls.sql — 27-qism #09 (DB-70, holat 09): Row Level Security —
-- har foydalanuvchi faqat o'z ma'lumotini ko'radi, BAZA darajasida.
-- Ilova `WHERE egasi_id = $1` ni unutsa ham boshqa odamning vazifasi
-- chiqmaydi (ikkinchi himoya qatlami).
-- Ilova har tranzaksiyada: BEGIN; SET LOCAL ilova.foydalanuvchi_id =
-- '<id>'; … COMMIT;  (SET emas — pool'da keyingi mijozga o'tadi;
-- o'lchandi).
-- Superuser va jadval egasi (postgres, migratsiya) RLS'ni chetlab
-- o'tadi — siyosatlar faqat `vazifalar_ilova` roli orqali ishlaydi.
-- Rollar klaster darajasida (baza o'chirilsa ham qoladi) — shuning
-- uchun IF NOT EXISTS.

DO $$ BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles
                 WHERE rolname = 'vazifalar_ilova') THEN
    CREATE ROLE vazifalar_ilova NOLOGIN;
  END IF;
END $$;

-- Joriy foydalanuvchi: o'rnatilmagan YOKI bo'sh satr → NULL (hech
-- narsa ko'rinmaydi — "yopiq holatda xavfsiz").
-- Bo'sh satr tuzog'i: SET LOCAL tugagach sessiyada '' qoladi,
-- ''::bigint → xato (o'lchandi).
CREATE FUNCTION joriy_foydalanuvchi() RETURNS bigint
LANGUAGE sql STABLE PARALLEL SAFE
RETURN nullif(
  current_setting('ilova.foydalanuvchi_id', true), '')::bigint;

GRANT USAGE ON SCHEMA public TO vazifalar_ilova;
GRANT SELECT, INSERT, UPDATE, DELETE
  ON foydalanuvchilar, vazifalar, teglar, vazifa_teglari, izohlar
  TO vazifalar_ilova;
-- tarix trigger'i yozadi, ilova o'qimaydi
GRANT INSERT ON vazifa_tarixi TO vazifalar_ilova;
GRANT SELECT ON faol_vazifalar, foydalanuvchi_statistikasi
  TO vazifalar_ilova;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO vazifalar_ilova;

ALTER TABLE foydalanuvchilar ENABLE ROW LEVEL SECURITY;
ALTER TABLE vazifalar ENABLE ROW LEVEL SECURITY;
ALTER TABLE teglar ENABLE ROW LEVEL SECURITY;
ALTER TABLE vazifa_teglari ENABLE ROW LEVEL SECURITY;
ALTER TABLE izohlar ENABLE ROW LEVEL SECURITY;

CREATE POLICY foydalanuvchilar_ozi ON foydalanuvchilar
  TO vazifalar_ilova
  USING (id = joriy_foydalanuvchi());
CREATE POLICY vazifalar_egasi ON vazifalar TO vazifalar_ilova
  USING (egasi_id = joriy_foydalanuvchi())
  -- boshqa odam nomiga yozib/ko'chirib bo'lmaydi
  WITH CHECK (egasi_id = joriy_foydalanuvchi());
CREATE POLICY teglar_egasi ON teglar TO vazifalar_ilova
  USING (egasi_id = joriy_foydalanuvchi())
  WITH CHECK (egasi_id = joriy_foydalanuvchi());
-- Bog'lanish va izohlar — vazifa orqali (EXISTS ichidagi vazifalar
-- ham RLS bilan filtrlanadi)
CREATE POLICY vazifa_teglari_egasi ON vazifa_teglari
  TO vazifalar_ilova
  USING (EXISTS (SELECT 1 FROM vazifalar v WHERE v.id = vazifa_id));
CREATE POLICY izohlar_vazifa_egasi ON izohlar TO vazifalar_ilova
  USING (EXISTS (SELECT 1 FROM vazifalar v WHERE v.id = vazifa_id))
  WITH CHECK (EXISTS (SELECT 1 FROM vazifalar v
                      WHERE v.id = vazifa_id)
              AND muallif_id = joriy_foydalanuvchi());

COMMENT ON FUNCTION joriy_foydalanuvchi() IS
  'SET LOCAL ilova.foydalanuvchi_id dan; o''rnatilmagan/bo''sh '
  '→ NULL (RLS hech narsa ko''rsatmaydi)';

Nimalarga e'tibor bering:

  • vazifa_tarixi ga faqat INSERT — ilova tarixni o'qiy olmaydi, faqat trigger yozadi. "Eng kam huquq" (least privilege).
  • DO $$ … $$ — Funksiyalar va PL/pgSQL darsidagi naqsh: rol klaster darajasida, baza qayta qurilsa ham qoladi.
  • vazifa_teglari siyosatida WITH CHECK yo'q — ALL siyosatda u holda USING sharti yozishga ham qo'llanadi.

Commit:

bash
git add sql/105-rls.sql
M="db: RLS — har foydalanuvchi faqat o'z ma'lumotini ko'radi;"
M+=" vazifalar_ilova roli, joriy_foydalanuvchi() (105)"
git commit -m "$M"

Halol eslatma: bu holatda vazifalar-api hali postgres (superuser) bilan ulanadi — siyosatlar API so'rovlariga ta'sir qilmaydi. API ulanadigan login roli (vazifalar_api, vazifalar_ilova a'zosi) — Rollar va huquqlar darsida (holat 11). API'ning har so'rovda set_config qilishi esa 28-qismda (autentifikatsiya: "kim kirgan" ni API tokendan biladi). Hozircha siz bazaning tayyor himoyasini qurdingiz va psql'da sinadingiz.

12. Real ishda

  • SaaS (ko'p ijarachili) tizimlar — har kompaniya faqat o'z ma'lumotini ko'radi; tenant_id + RLS — klassik naqsh.
  • Supabase to'liq RLS'ga tayanadi: brauzer to'g'ridan-to'g'ri bazaga so'rov yuboradi va himoyaning hammasi siyosatlarda. U yerda "RLS'ni yoqishni unutdim" — eng ko'p uchraydigan sizish sababi (platformalarning ogohlantirishlari ham shu haqida).
  • Audit va muvofiqlik (shaxsiy ma'lumotlar qonunchiligi) — "bazaning o'zi kafolatlaydi" degan argument tekshiruvchilar uchun muhim.
  • Intervyu: "Multi-tenant tizimda ma'lumotni qanday ajratasiz?", "RLS'ning kamchiliklari?", "Pool bilan qanday ishlaydi?".

Xulosa

  • RLS: ENABLE ROW LEVEL SECURITY + CREATE POLICY; siyosat bo'lmasa — hech narsa ko'rinmaydi (fail closed).
  • USING — mavjud qatorlarni filtrlaydi (begona UPDATE/DELETE → 0), WITH CHECK — yozilayotgan qatorni tekshiradi (xato).
  • Joriy foydalanuvchi — set_config('…', $1, true) faqat tranzaksiya ichida; SET (LOCAL'siz) pool'da sizadi; nullif(…, '') — bo'sh satr tuzog'iga qarshi.
  • Superuser va BYPASSRLS chetlab o'tadi, egasi — FORCE siz; ilova cheklangan rol bilan ulanishi shart.
  • Oddiy siyosat deyarli bepul; EXISTS li siyosat katta jadvalda 100 barobar sekin bo'lishi mumkin — RLS WHERE ning o'rnini emas, sug'urtasini bajaradi.

Keyingi dars: PostGIS va geo so'rovlar — "menga eng yaqin 10 ta filial" so'rovini to'g'ri va tez yozish.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL Row Level Security: CREATE POLICY, SET LOCAL va pool tuzoqlari — IlmHamroh