Mundarija (37)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi siyosat
- 2.1 Ikki qadam: yoqish va siyosat
- 2.2 Kanon siyosatlari
- 3. joriy_foydalanuvchi() qayerdan biladi
- 3.1 Sozlama — tranzaksiya "yorlig'i"
- 3.2 Superuser, ilova roli, SET LOCAL
- 3.3 Ilova kodidan: set_config
- 4. USING va WITH CHECK
- 4.1 Begona qatorni o'zgartirish — 0, xato emas
- 4.2 WITH CHECK — yangi qator uchun
- 4.3 Trigger'lar RLS ostida
- 4.4 Bir nechta siyosat: PERMISSIVE va RESTRICTIVE
- 5. Tuzoqlar: bo'sh satr va pool
- 5.1 Bo'sh satr tuzog'i
- 5.2 SET (LOCAL'siz) — keyingi mijozga sizadi
- 6. Kim chetlab o'tadi: superuser, egasi, FORCE, BYPASSRLS
- 7. Hujumchi nigohi
- 8. Narx
- 8.1 Oddiy siyosat — deyarli bepul
- 8.2 Subquery'li siyosat — qimmat bo'lishi mumkin
- 9. Ko'p uchraydigan xatolar
- 9.1 Ilova superuser bilan ulangan
- 9.2 SET o'rniga SET LOCAL
- 9.3 SET LOCAL … = '${id}' — satr ulash
- 9.4 WITH CHECK siz siyosat
- 9.5 UPDATE 0 ni muvaffaqiyat deb hisoblash
- 9.6 nullif siz current_setting(…)::bigint
- 10. Mashqlar
- 1-mashq (oson): Default deny
- 2-mashq (o'rta): Hamyon siyosati
- 3-mashq (qiyin): Pool sizishini ko'rsating
- 11. Loyiha qadami: RLS migratsiyasi (holat 09)
- 12. Real ishda
- Xulosa
- Manbalar
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 foydalanuvchibo'lgan qatorlarni ko'radi". JadvaldaENABLE ROW LEVEL SECURITY, keyinCREATE POLICY … USING (…) WITH CHECK (…). Joriy foydalanuvchini ilova har tranzaksiyadaSET LOCAL(yokiset_config(…, true)) bilan beradi. IlovaWHEREni 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 SECURITYvaCREATE POLICYbilan qatorlar darajasidagi himoyani yoqasiz.USINGvaWITH CHECKfarqini, nega begona qatorni UPDATE qilish xato emas,0berishini tushuntirasiz.- Joriy foydalanuvchini
SET LOCAL/set_configbilan xavfsiz uzatasiz va bo'sh satr tuzog'ini bilasiz. - Superuser, jadval egasi,
FORCEvaBYPASSRLSqoidalarini 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.)
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;BEGIN
CREATE TABLE
INSERT 0 2
GRANT
ALTER TABLE
SET
count
-------
0
(1 row)
ROLLBACKGRANT 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:
SELECT tablename, policyname, cmd, roles, qual
FROM pg_policies ORDER BY 1, 2; 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:
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:
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:
SELECT current_user,
(SELECT rolbypassrls FROM pg_roles
WHERE rolname = current_user) AS bypass,
count(*) FROM vazifalar; 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:
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;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 = NULLhech 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—WHEREyo'q, lekin faqat1. 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:
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = $1 \bind '1' \g
ROLLBACK;
BEGIN;
SELECT set_config('ilova.foydalanuvchi_id', $1, true) \bind '1' \g
ROLLBACK;BEGIN
ERROR: syntax error at or near "$1"
LINE 1: SET LOCAL ilova.foydalanuvchi_id = $1
^
ROLLBACK
BEGIN
set_config
------------
1
(1 row)
ROLLBACKset_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):
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_configalohidapool.query()bilan, asosiy so'rov esa boshqapool.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:
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;SET
BEGIN
SET
UPDATE 0
DELETE 0
ROLLBACK3-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:
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;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"
ROLLBACKTarjimasi: "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:
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;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"
ROLLBACKtartib = 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 DEFINERbo'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:
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;BEGIN
CREATE POLICY
SET
SET
count
-------
12
(1 row)
ROLLBACK13 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:
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;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:
SET ROLE vazifalar_ilova;
SET ilova.foydalanuvchi_id = '1';
SELECT count(*) AS keyingi_sorov_korgani FROM vazifalar;
RESET ilova.foydalanuvchi_id;
RESET ROLE;SET
SET
keyingi_sorov_korgani
-----------------------
13
(1 row)
RESET
RESETSET — 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), faqatBEGIN … COMMITichida. - PgBouncer transaction rejimida
SET(sessiya) umuman ishonchsiz — ulanishlar tranzaksiya oxirida almashadi (PgBouncer va tashqi pooler'lar);SET LOCALesa 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_configga 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:
ALTER TABLE teglar FORCE ROW LEVEL SECURITY;
SELECT count(*) AS superuser_force_bilan FROM teglar;
ALTER TABLE teglar NO FORCE ROW LEVEL SECURITY;ALTER TABLE
superuser_force_bilan
-----------------------
32
(1 row)
ALTER TABLE32 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:
GET /v1/vazifalar/3(begona id) — ilovaWHERE egasi_idni unutgan endpoint. RLS: 0 qator → 404. Sizish yo'q.PATCH /v1/vazifalar/1 {"egasiId": 2}— o'z vazifasini begonaga "tashlash" (spam).WITH CHECK→ xato.POST /v1/vazifalar/3/izohlar— begona vazifaga izoh. Izohlar siyosatidagiEXISTS→ xato.- 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:
SET ROLE vazifalar_ilova;
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
EXPLAIN (COSTS OFF) SELECT i.id FROM izohlar i;
COMMIT;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)
COMMITPostgres 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:
- 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
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):
mijoz: 0
superuser: 2Siyosat 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
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):
t
balans: 50000
begona yangilandi: 0
rad: new row violates row-level security policy for table "m09_hamyon"
Dilnoza: 120000Begona 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
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):
t
1-sorov: 50000
t
2-sorov: 50000
3-sorov: 50000Uchinchi 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):
-- 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_tarixiga faqatINSERT— 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_teglarisiyosatidaWITH CHECKyo'q —ALLsiyosatda u holdaUSINGsharti yozishga ham qo'llanadi.
Commit:
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
BYPASSRLSchetlab o'tadi, egasi —FORCEsiz; ilova cheklangan rol bilan ulanishi shart. - Oddiy siyosat deyarli bepul;
EXISTSli siyosat katta jadvalda 100 barobar sekin bo'lishi mumkin — RLSWHEREning 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
- PostgreSQL 18 hujjati: Row Security Policies — 2026-10-25 da ko'rildi.
- PostgreSQL 18 hujjati: CREATE POLICY (PERMISSIVE/RESTRICTIVE,
USING,WITH CHECK). - PostgreSQL 18 hujjati: System Administration Functions — set_config, SET.
- OWASP Top 10:2025 — A01 Broken Access Control: owasp.org/Top10 — 2026-10-25 da ko'rildi.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!