IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi20/31-dars29 daqiqa
Mundarija (40)

RBAC va resurs egaligi: rollar, 401 va 403, IDOR testlari va RLS

Qisqacha: Avtorizatsiya — kirgan odam nima qila olishini tekshirish. Ikki savol bor: "uning roli bunga ruxsat beradimi?" (RBAC) va "bu resurs unikimi?" (egalik). Ikkinchisini unutish — API'lardagi eng ko'p uchraydigan zaiflik: IDOR (BOLA), ya'ni URL'dagi raqamni o'zgartirib, birovning ma'lumotini olish. Himoya: har so'rovda egalik shartini qo'yish, rolni bazadan olish va har resurs uchun "begona foydalanuvchi — 404" testini yozish. Kanon bundan tashqari bazaning o'zini ham ikkinchi qatlam qildi: API endi superuser emas, RLS ostidagi oddiy rol bilan ulanadi.

Bu darsda

  • Autentifikatsiya va avtorizatsiya farqini, 401, 403 va 404 qachon qaytishini tushuntira olasiz.
  • Rol, ruxsat (permission) va rol iyerarxiyasini kodda ifodalaysiz.
  • IDOR (BOLA) hujumini ko'rasiz va unga qarshi test yozasiz.
  • API'ni RLS ostida, ikki xil cheklangan rol bilan ulaysiz.
  • vazifalar-api ga admin yo'llari, bloklash va imzolangan kursorni qo'shasiz.

Oldin bilishingiz kerak: Session-based auth, Status kodlarni to'g'ri tanlash, Row Level Security, Rollar va huquqlar, Ikki faktorli autentifikatsiya va passkeys.

Oldingi o'n beshta darsda bitta savolga javob berdik: "bu kim?". Parol, sessiya, SMS, Google, Telegram, 2FA — hammasi autentifikatsiya. Bugun ikkinchi savol: "bu odam shuni qila oladimi?".

1. Nega bu kerak?

1.1 Kirgan — hali hammasi mumkin degani emas

Parolni xavfsiz saqlash darsida ikki so'zni ajratgan edik: autentifikatsiya (kimligini tasdiqlash) va avtorizatsiya (ruxsat). Aeroportdagi o'xshatish: pasport nazorati — autentifikatsiya; bortga chiqish talonidagi o'rindiq raqami — avtorizatsiya. Pasportingiz haqiqiy bo'lsa ham, biznes klassga o'tira olmaysiz.

vazifalar-api da avtorizatsiya ikki xil bo'ladi:

  • funksiya darajasida: "admin sahifasiga kim kira oladi?" — bu rolga bog'liq;
  • obyekt darajasida: "42-vazifani kim o'qiy oladi?" — bu egalikka bog'liq.

1.2 OWASP ro'yxatining birinchi qatori

OWASP API Security Top 10 (2023) ro'yxatida birinchi o'rinda API1: Broken Object Level Authorization (BOLA) turibdi. Uning eski, keng tarqalgan nomi — IDOR (Insecure Direct Object Reference). Mexanizmi juda oddiy:

http
GET /v1/vazifalar/1   ← Aziz o'z vazifasini oladi
GET /v1/vazifalar/5   ← Aziz raqamni o'zgartirdi

Agar server ikkinchi so'rovda ham 200 qaytarsa — bu IDOR. Hech qanday murakkab hujum kerak emas, faqat brauzer manzil qatori. Funksiya darajasidagi xato (oddiy foydalanuvchi admin yo'liga kira oladi) esa API5: Broken Function Level Authorization (BFLA) deyiladi.

1.3 Bizning kodimizda ham shunday teshiklar bor edi

28-qism kanonini qurishda haqiqiy zaifliklar topildi — hammasi oldingi qismlardagi bizning kodimizda:

  1. Idempotency-Key (Idempotency-Key darsi) hamma foydalanuvchi uchun umumiy edi. Dilnoza Azizning kalitini yuborsa, Azizning saqlangan javobini olardi.
  2. Eksport ishlari egasiz edi: havolani bilgan har kim boshqaning eksport faylini yuklab olardi.
  3. 27-qismdan beri API bazaga superuser (postgres) bilan ulanardi. Row Level Security darsidagi siyosatlar bu rolga ishlamaydi.

Birinchi ikkitasi sessiya darsi bilan birga tuzatilgan va testlari bor (pastda ko'rasiz). Uchinchisi — bugungi darsning markazi.

2. RBAC: rol va ruxsat

2.1 Tushunchalar

RBAC (Role-Based Access Control) — rolga asoslangan ruxsat:

  • ruxsat (permission) — bitta aniq amal: vazifa:oqish, hisob:bloklash;
  • rol (role) — ruxsatlar to'plami: foydalanuvchi, admin;
  • foydalanuvchiga ruxsat emas, rol beriladi.

Nega bunday bilvosita? Yangi xodim kelsa, unga 15 ta ruxsatni alohida berish o'rniga bitta rol berasiz. "Adminlar endi hisoblarni o'chira olsin" desangiz — bitta rolni o'zgartirasiz.

2.2 Rol iyerarxiyasi

Ko'pincha rollar bir-birini meros qiladi: admin oddiy foydalanuvchi qila oladigan hamma narsani qila oladi va yana bir oz ko'proq. Buni rol iyerarxiyasi deyishadi.

js
const ROLLAR = {
  mehmon: { meros: [], ruxsatlar: ["vazifa:oqish-ommaviy"] },
  foydalanuvchi: {
    meros: ["mehmon"],
    ruxsatlar: ["vazifa:oqish", "vazifa:yozish"],
  },
  admin: {
    meros: ["foydalanuvchi"],
    ruxsatlar: ["hisob:royxat", "hisob:bloklash"],
  },
};

function ruxsatlari(rol, korilgan = new Set()) {
  if (korilgan.has(rol)) return new Set();
  korilgan.add(rol);
  const r = ROLLAR[rol];
  const hammasi = new Set(r.ruxsatlar);
  for (const ota of r.meros) {
    for (const x of ruxsatlari(ota, korilgan)) hammasi.add(x);
  }
  return hammasi;
}

const mumkinmi = (rol, ruxsat) => ruxsatlari(rol).has(ruxsat);

console.log([...ruxsatlari("admin")].join(", "));
console.log(mumkinmi("foydalanuvchi", "vazifa:yozish"));
console.log(mumkinmi("foydalanuvchi", "hisob:bloklash"));
console.log(mumkinmi("admin", "vazifa:oqish"));

Konsolda:

text
hisob:royxat, hisob:bloklash, vazifa:oqish, vazifa:yozish, vazifa:oqish-ommaviy
true
false
true

Admin o'z ruxsatlaridan tashqari foydalanuvchi va mehmon ruxsatlarini ham oldi. korilgan to'plami — ehtiyot chorasi: kimdir xato bilan admin ni foydalanuvchi ning merosiga ham yozsa, cheksiz rekursiya bo'lmaydi.

Kanon vazifalar-api bundan soddaroq: ikkita rol (foydalanuvchi, admin) va har yo'lda "qaysi rollarga ruxsat" ro'yxati. Kichik loyihada bu yetarli. Ruxsatlar jadvali va iyerarxiya rollar soni o'sganda kerak bo'ladi.

Diqqat: Frontend'dagi tekshiruv ("admin bo'lmasa tugmani yashir") — faqat qulaylik (UX) uchun. Tugmani yashirish yo'lni yopmaydi: kimdir curl bilan to'g'ridan-to'g'ri so'rov yuboradi. Har ruxsat serverda tekshiriladi.

3. 401, 403 va 404

3.1 Uch xil "yo'q"

Status kodlarni to'g'ri tanlash darsidagi ikki kod endi to'liq ma'no oladi:

Kod Ma'nosi Qachon
401 Unauthorized "kimligingizni bilmayman" sessiya yo'q yoki yaroqsiz
403 Forbidden "bilaman, lekin ruxsat yo'q" rol mos emas
404 Not Found "bunday narsa yo'q" begona resurs ham

Nomlar chalkash: 401 "Unauthorized" deyilgani bilan, aslida "unauthenticated" ma'nosini beradi. Bu HTTP tarixidagi qoldiq.

js
function holat(sessiya, kerakliRollar) {
  if (sessiya === undefined) return 401;
  if (!kerakliRollar.includes(sessiya.rol)) return 403;
  return 200;
}

const SOROVLAR = [
  ["mehmon", undefined],
  ["Aziz", { id: 1, rol: "foydalanuvchi" }],
  ["Bekzod", { id: 7, rol: "admin" }],
];
for (const [kim, s] of SOROVLAR) {
  console.log(kim, holat(s, ["admin"]));
}

Konsolda:

text
mehmon 401
Aziz 403
Bekzod 200

Tartib muhim: avval "kim?", keyin "ruxsat bormi?". Kirmagan odamga 403 qaytarsangiz, u "kirsam ham bo'lmaydi" deb o'ylaydi — aslida u shunchaki kirishi kerak.

3.2 Begona resurs — nega 404, 403 emas

Dilnoza Azizning 1-vazifasini so'radi. To'g'ri javob 403 ("ruxsat yo'q") emasmi?

403 bir narsani oshkor qiladi: "1-vazifa bor, lekin sizniki emas". Hujumchi 1 dan 1 000 000 gacha so'rab, qaysi id'lar bandligini, nechta vazifa borligini bilib oladi. 404 esa hech narsa aytmaydi: "sizning dunyongizda bunday vazifa yo'q". Kanon qoidasi: begona resurs — 404. 403 faqat funksiya darajasida (rol mos emas) qaytadi — u yerda yo'l mavjudligi sir emas.

Bitta so'rov uchala savoldan shu tartibda o'tadi:

flowchart LR
  S[So'rov] --> A{Sessiya bormi?}
  A -->|yo'q| K401[401]
  A -->|ha| B{Rol mos keladimi?}
  B -->|yo'q| K403[403]
  B -->|ha| C{"Resurs topildimi? (egasi_id va RLS)"}
  C -->|yo'q yoki begona| K404[404]
  C -->|ha| K200[200]

4. IDOR: hujum va himoya

4.1 Hujum bitta qatorda

IDOR odatda shunday kodda tug'iladi:

js
const VAZIFALAR = [
  { id: 1, egasiId: 1, matn: "Azizning vazifasi" },
  { id: 5, egasiId: 2, matn: "Dilnozaning vazifasi" },
];

// zaif: faqat id bo'yicha
function olZaif(id) {
  return VAZIFALAR.find((v) => v.id === id);
}

// to'g'ri: id VA egasi bo'yicha
function ol(id, egasiId) {
  return VAZIFALAR.find((v) => v.id === id && v.egasiId === egasiId);
}

const javob = (v) => (v === undefined ? "404" : `200 ${v.matn}`);

const aziz = 1;
console.log(javob(olZaif(5)));
console.log(javob(ol(5, aziz)));
console.log(javob(ol(1, aziz)));
console.log(javob(ol(999, aziz)));

Konsolda:

text
200 Dilnozaning vazifasi
404
200 Azizning vazifasi
404

Birinchi qator — IDOR: Aziz Dilnozaning vazifasini o'qidi. To'g'ri funksiyada begona id va mavjud bo'lmagan id bir xil javob beradi (404) — tashqaridan ularni farqlab bo'lmaydi.

SQL'da bu WHERE id = $1 o'rniga WHERE id = $1 AND egasi_id = $2. vazifalar-api da Sessiya darsidan beri PgOmbori har so'rovda egasi_id ni qo'shadi: ombor obyekti foydalanuvchi id bilan yaratiladi va boshqasini bilmaydi.

4.2 Qayerlarda unutiladi

Asosiy CRUD yo'llarida egalikni deyarli hamma eslaydi. Teshiklar chetda qoladi:

  • yordamchi jadvallar: idempotentlik keshi, eksport ishlari, fayllar, bildirishnomalar — kanon topgan ikkala BOLA aynan shunday edi;
  • ommaviy amallar: PUT /vazifalar (hammasini almashtirish) ichidagi id'lar;
  • ichki havolalar: GET /eksportlar/abc123/fayl — "havolani hech kim bilmaydi" degan taxmin;
  • yangi yo'l: kod nusxalanadi, egalik sharti esa yo'qoladi.

Shuning uchun egalikni ko'z bilan emas, test bilan tekshiramiz.

4.3 IDOR testlari

Kanon test/sessiya.test.ts da alohida to'plam bor — "foydalanuvchilar ajratilgan (IDOR / BOLA)". G'oyasi: ikki foydalanuvchi, biri resurs yaratadi, ikkinchisi uni har usul bilan olishga urinadi. Birinchi test (telefon uchun bo'lingan):

ts
test("Aziz va Dilnoza: har biri faqat o'z vazifalari; begona id — " +
  "404 (403 emas); PATCH/DELETE begonaga — 404", async () => {
  const aziz = await royxat("aziz@example.com");
  const dilnoza = await royxat("dilnoza@example.com");
  const v = (await request(app).post("/v1/vazifalar")
    .set("Cookie", aziz).send({ matn: "Azizniki #maxfiy" })
    .expect(201)).body as { id: number };
  await request(app).post("/v1/vazifalar").set("Cookie", dilnoza)
    .send({ matn: "Dilnozaniki" }).expect(201);
  const royxatD = (await request(app).get("/v1/vazifalar")
    .set("Cookie", dilnoza).expect(200)).body as {
      vazifalar: { matn: string }[];
    };
  assert.deepEqual(royxatD.vazifalar.map((x) => x.matn),
    ["Dilnozaniki"]);
  const yol = `/v1/vazifalar/${String(v.id)}`;
  await request(app).get(yol).set("Cookie", dilnoza).expect(404);
  await request(app).patch(yol).set("Cookie", dilnoza)
    .send({ bajarildi: true }).expect(404);
  await request(app).delete(yol).set("Cookie", dilnoza).expect(404);
  const teglar = (await request(app).get("/v1/teglar")
    .set("Cookie", dilnoza).expect(200)).body as {
      teglar: unknown[];
    };
  assert.deepEqual(teglar.teglar, []);
  const a = (await request(app).get(yol).set("Cookie", aziz)
    .expect(200)).body as { bajarildi: boolean };
  assert.equal(a.bajarildi, false);
});

E'tibor bering, test faqat "o'qiy olmaydi" bilan to'xtamaydi:

  • ro'yxatda begona vazifa yo'q;
  • GET, PATCH, DELETE — uchalasi 404;
  • teglar ro'yxatida Azizning #maxfiy tegi ko'rinmaydi (yon kanal orqali sizish);
  • oxirida Aziz o'z vazifasini o'zgarmagan holda ko'radi — Dilnozaning PATCH urinishi jim muvaffaqiyat bo'lmagan.

Biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z PostgreSQL 17.6 bazamiz), faqat shu to'plamni:

bash
node --test --test-name-pattern="IDOR" test/sessiya.test.ts

Chiqishda to'rtta ✔ qatoriga qarang — har biri bitta resurs turida begona foydalanuvchini to'xtatadi:

text
▶ foydalanuvchilar ajratilgan (IDOR / BOLA)
  ✔ Aziz va Dilnoza: har biri faqat o'z vazifalari; begona id — 404 (403 emas); PATCH/DELETE begonaga — 404 (692.5733ms)
  ✔ Idempotency-Key — foydalanuvchi bo'yicha: Dilnoza Azizning kalitini ishlatsa Azizning javobini OLMAYDI (498.517ms)
  ✔ eksport ishi — faqat yaratgan foydalanuvchiga (begona — 404) (601.3384ms)
  ✔ yashirin yo'llar yo'q: /eksportlar, /auth versiyasiz — 404 (faqat /vazifalar, /teglar eski yo'l sifatida qoldi) (448.51ms)
✔ foydalanuvchilar ajratilgan (IDOR / BOLA) (2243.1847ms)
ℹ tests 4
ℹ suites 1
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4549.3041

Nima bo'ldi? Vazifalar, Idempotency-Key va eksport ishlarida begona foydalanuvchi 404 oldi — yuqoridagi uchta teshikdan ikkitasi test bilan yopiq. To'rtinchi test — yana bir topilma: /v1 router versiyasiz ildizda ham ochiq edi, ya'ni /eksportlar /v1/eksportlar bilan bir xil ishlardi. Bunday "unutilgan" yo'llar OWASP API ro'yxatida alohida band — API9 (inventarni noto'g'ri boshqarish).

Maslahat: Har yangi resurs (izohlar, fayllar, kalitlar) uchun shu shablonni nusxa qiling: "ikki foydalanuvchi, begona — 404, ro'yxatda yo'q". Bu test bir marta yoziladi va har refaktoringda sizni himoya qiladi.

5. Bazaning o'zi ikkinchi qatlam: RLS ostida API

5.1 Muammo: API superuser edi

Egalik sharti kodda. Lekin bitta yangi so'rovda WHERE egasi_id unutilsa-chi? Row Level Security darsida bazaga siyosat qo'ygan edik: vazifalar_ilova roli faqat egasi_id = joriy_foydalanuvchi() qatorlarni ko'radi. Lekin API qaysi rol bilan ulanardi? Quyidagi so'rovlar kanon sxemasi (119–120 migratsiyalari bilan) to'ldirilgan lokal bazada bajarildi (biz ishga tushirdik, PostgreSQL 17.6):

sql
SELECT usename, rolsuper, rolbypassrls
FROM pg_user JOIN pg_roles ON rolname = usename
WHERE usename = current_user;

Natija:

text
 usename  | rolsuper | rolbypassrls
----------+----------+--------------
 postgres | t        | t
(1 row)

Bu — bizning psql sessiyamiz, lekin 27-qism DATABASE_URL ham aynan postgres foydalanuvchisi edi (kanon jonli API'da tekshirdi). rolbypassrls = t — RLS siyosatlari bu rolga umuman ta'sir qilmaydi:

sql
SELECT count(*) AS jami, count(DISTINCT egasi_id) AS egalar
FROM vazifalar;

Natija:

text
 jami | egalar
------+--------
   48 |      7
(1 row)

WHERE siz so'rov — yetti kishining 48 ta vazifasi. RLS bor edi, lekin qulf eshikka emas, devorga osilgan edi.

5.2 Yechim: ikki rol, ikki hovuz (pool)

Kanon migratsiyasi 119-rollar-rls-api.sql API'ni ikki cheklangan rolga ajratdi — eng kam huquq tamoyili (Xavfsizlik tamoyillari):

Rol Nima qila oladi Nima qila olmaydi
vazifalar_api vazifalar, RLS ostida auth jadvallari, CREATE
vazifalar_auth foydalanuvchilar, sessiyalar, sirlar vazifalar

Nega ikkita? Login paytida server foydalanuvchini email bo'yicha topishi kerak — u hali kimligini bilmaydi, RLS ostida esa hech narsa ko'rinmaydi. Shuning uchun auth uchun alohida rol, uning foydalanuvchilar jadvalida o'z siyosati bor (hamma qator). Lekin bu rol vazifalarni umuman ko'rmaydi. Ma'lumot roli esa sessiya tokenlari va 2FA sirlarini ko'rmaydi.

Endi vazifalar_api bilan:

sql
SET ROLE vazifalar_api;
SELECT count(*) FROM vazifalar;

Natija:

text
SET
 count
-------
     0
(1 row)

set_config siz — nol qator. Bu "yopiq holatda xavfsiz" (fail closed): kod foydalanuvchini o'rnatishni unutsa, hech narsa ko'rinmaydi, birovniki ham.

sql
BEGIN;
SET LOCAL ROLE vazifalar_api;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
SELECT count(*) AS jami, count(DISTINCT egasi_id) AS egalar
FROM vazifalar;
ROLLBACK;

Natija:

text
BEGIN
SET
 set_config
------------
 1
(1 row)

 jami | egalar
------+--------
   13 |      1
(1 row)

ROLLBACK

Xuddi o'sha WHERE siz so'rov endi faqat Azizning 13 ta vazifasini qaytaradi. set_config(..., true) — uchinchi argument true = faqat shu tranzaksiya uchun (SET LOCAL ning funksiya ko'rinishi; SET LOCAL o'zi parametr qabul qilmaydi).

Rollar chegarasi:

sql
SET ROLE vazifalar_api;
SELECT count(*) FROM sessiyalar;

Natija:

text
SET
ERROR:  permission denied for table sessiyalar
sql
SET ROLE vazifalar_auth;
SELECT count(*) FROM vazifalar;

Natija:

text
SET
ERROR:  permission denied for table vazifalar

5.3 Topilma: default privileges hammasini berib qo'ygan edi

Yuqoridagi permission denied — 119 dan keyin. Undan oldin (sessiya darsidan shu darsgacha) vazifalar_api sessiya tokenlari hash'larini, shifrlangan TOTP sirlarini va OTP kodlarini o'qiy olardi. Sabab — 27-qismdagi 106-rollar.sql:

sql
ALTER DEFAULT PRIVILEGES FOR ROLE CURRENT_USER IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO vazifalar_ilova;

Bu buyruq "keyin yaratiladigan har jadvalga ham shu huquqlar" degani. 28-qismdagi har yangi auth jadvali avtomatik ravishda ma'lumot roliga ochilgan. O'zingiz ko'ring — hozir ham yangi jadval yaratsak:

sql
CREATE TABLE sinov_sirlar (x text);
SELECT string_agg(privilege_type, ', ' ORDER BY privilege_type)
FROM information_schema.role_table_grants
WHERE grantee = 'vazifalar_ilova' AND table_name = 'sinov_sirlar';
DROP TABLE sinov_sirlar;

Natija:

text
CREATE TABLE
           string_agg
--------------------------------
 DELETE, INSERT, SELECT, UPDATE
(1 row)

DROP TABLE

Hech kim GRANT yozmadi, lekin to'rtta huquq tayyor. Qulaylik uchun qo'yilgan sozlama xavfsizlik teshigiga aylandi. 119 ning yechimi — auth jadvallaridan REVOKE ALL ... FROM vazifalar_ilova, keyingi har auth migratsiyasida ham (masalan, keyingi darsdagi api_kalitlar) shu qator.

Bu teshik xato bermasdi — shuning uchun uzoq yashadi. Kanon uni information_schema.role_table_grants ni ko'rib chiqib topdi. Saboq: huquqlarni "ishlayapti" bilan emas, "ortiqcha nima bor" bilan tekshiring.

5.4 Topilma: RLS ostida PUT 500 berardi

RLS yoqilgach yana bir yashirin xato chiqdi. PUT /v1/vazifalar (hammasini almashtirish) mijoz bergan id'larni ishlatadi. Agar id boshqa foydalanuvchiniki bo'lsa — yangi id beriladi. Buni aniqlash uchun eski kod "begona id'larni" SELECT qilardi. RLS ostida esa begona qator ko'rinmaydi — so'rov bo'sh qaytadi, kod "bo'sh id" deb o'ylaydi va INSERT qiladi:

text
"begona id'lar" so'rovi: 0 qator (RLS B ning qatorini yashirdi — kod uni "bo'sh id" deb o'ylaydi)
INSERT → duplicate key value violates unique constraint "vazifalar_pkey"  ⇒ API 500 bo'lardi

Bu — kanon tajribasining chiqishi. Yechim — faqat "band-mi?" savoliga javob beradigan funksiya: vazifa_idlari_band(bigint[]). U SECURITY DEFINER (egasining huquqi bilan ishlaydi, RLS'siz), lekin faqat id'larni qaytaradi, mazmunni emas:

sql
BEGIN;
SET LOCAL ROLE vazifalar_api;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
SELECT id FROM vazifalar WHERE id IN (1, 5, 999);
SELECT vazifa_idlari_band('{1, 5, 999}');
ROLLBACK;

Natija:

text
BEGIN
SET
 set_config
------------
 1
(1 row)

 id
----
  1
(1 row)

 vazifa_idlari_band
--------------------
 {1,5}
(1 row)

ROLLBACK

Oddiy SELECT faqat 1 ni ko'rdi (5 — Dilnozaniki). Funksiya esa "1 va 5 band, 999 bo'sh" dedi. SECURITY DEFINER funksiyada SET search_path = pg_catalog, public qotirilgan — aks holda chaqiruvchi o'z sxemasidagi soxta vazifalar jadvalini qo'yib, funksiyani aldashi mumkin edi. EXECUTE huquqi faqat vazifalar_ilova ga.

Va nihoyat, RLS'ning WITH CHECK qismi — begona nomiga yozish:

sql
\set VERBOSITY terse
BEGIN;
SET LOCAL ROLE vazifalar_api;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
INSERT INTO vazifalar (egasi_id, matn) VALUES (2, 'begona nomiga');
ROLLBACK;

Natija:

text
BEGIN
SET
 set_config
------------
 1
(1 row)

ERROR:  new row violates row-level security policy for table "vazifalar"
ROLLBACK

"Yangi qator vazifalar jadvalidagi RLS siyosatini buzadi". Kod egasi_id ni noto'g'ri qo'ysa ham, baza rad etadi.

5.5 Narxi

Endi har o'qish ham tranzaksiyada: BEGIN + set_config + so'rov + COMMIT. Kanon o'lchadi (400 marta ketma-ket, mediana):

Bitta vazifani o'qish: mediana (400 marta)
  • pool.query, tranzaksiyasizeski, superuser1,01 ms
  • BEGIN+set_config+so'rov+COMMITsuperuser3,24 ms
  • xuddi shu, vazifalar_apiRLS ostida4,2 ms

Manba: kanon o'lchovi (IlmHamroh, 2026-10): PostgreSQL 18.6 Docker Desktop orqali, Node 24.21.0, i5-12500H, Windows 11, 2026-10-28

Bitta vazifa 1.0 ms dan 3.2 ms ga oshdi — lekin bu superuser bilan ham. Ya'ni narx RLS siyosatida emas, uchta qo'shimcha tarmoq aylanishida (har biri Docker Desktop'da ~0.7 ms). 50 ta vazifali ro'yxatda farq yo'qoldi. Superuser bilan 4.87 ms, RLS ostida 4.25 ms. set_config ni asosiy so'rov ichiga (CTE) qo'shib aylanishlarni kamaytirish mumkin edi — kanon buni qilmadi: Postgres siyosat va CTE baholash tartibini kafolatlamaydi.

Tekshirib ko'ring: Kodda WHERE egasi_id = $1 baribir qolgan. RLS bor ekan, uni olib tashlasa bo'ladimi?

Javob

Yo'q. Bu — chuqur himoya (defense in depth): ikki mustaqil qatlam. Ertaga kimdir test uchun superuser bilan ulansa yoki migratsiya siyosatni tasodifan o'chirsa, WHERE qoladi. Kod sharti qolsa, RLS qo'shimcha narx bermaydi — kanon o'lchovida farq tarmoq aylanishlaridan edi.

6. Admin yo'llari

6.1 Rol har so'rovda bazadan

Rol qayerda saqlanadi? foydalanuvchilar.rol ustunida (foydalanuvchi | admin, CHECK bilan). Sessiya tekshiruvi har so'rovda sessiyalar va foydalanuvchilarni JOIN qiladi va rolni ham oladi. Natija: adminlikdan olingan odam o'sha sessiya bilan keyingi so'rovdayoq 403 oladi.

Rolni JWT ichiga yozganda bunday emas: token muddati tugaguncha eski rol amal qiladi. Bu Session vs JWT qarori darsidagi kanon tanlovining yana bir sababi.

6.2 rolKerak va admin router

Kanon src/auth/admin.ts (telefon uchun bo'lingan, izohlar qisqartirilgan; bo'lingan nusxa tip va lint'dan o'tdi — biz tekshirdik):

ts
// admin.ts — RBAC: administrator yo'llari. Rol sessiyadan (bazadan,
// har so'rovda). 401 — kim ekani noma'lum; 403 — kimligi ma'lum,
// RUXSATI yo'q. Admin boshqalarning VAZIFALARINI ko'rmaydi
// (maxfiylik) — faqat hisoblarni boshqaradi. Har amal jurnalga.
import { Router } from "express";
import type { RequestHandler } from "express";
import { z } from "zod";
import type { Pool } from "pg";
import { tekshirib } from "../umumiy/tekshir.ts";
import { IlovaXatosi, TopilmadiXatosi } from "../umumiy/xatolar.ts";
import { TURLAR } from "../vazifalar/pg-ombor.ts";
import { KirilmaganXatosi } from "./middleware.ts";
import type { AuthOmbori } from "./ombor.ts";
import type { SessiyaOmbori } from "./sessiya.ts";

export type Rol = "foydalanuvchi" | "admin";

export class RuxsatYoqXatosi extends IlovaXatosi {
  constructor() {
    super(403, "Bu amal uchun ruxsatingiz yo'q", {
      kod: "ruxsat-yoq",
      sarlavha: "Ruxsat yo'q",
    });
  }
}

/** Rol talabi: sessiya yo'q → 401, rol mos emas → 403 */
export function rolKerak(...rollar: readonly Rol[]): RequestHandler {
  return (req, _res, next) => {
    if (req.sessiya === undefined) throw new KirilmaganXatosi();
    if (!rollar.includes(req.sessiya.rol)) {
      throw new RuxsatYoqXatosi();
    }
    next();
  };
}

const Id = z.strictObject({
  id: z.coerce.number().int().positive().max(Number.MAX_SAFE_INTEGER),
});
const Royxat = z.strictObject({
  limit: z.coerce.number().int().min(1).max(100).default(20),
  keyin: z.coerce.number().int().nonnegative().default(0),
});

export function adminRouteri(
  pool: Pool,
  ombor: AuthOmbori,
  sessiyalar: SessiyaOmbori,
): Router {
  const r = Router();
  r.use(rolKerak("admin"), (_req, res, next) => {
    res.set("Cache-Control", "no-store");
    next();
  });

  // Hisoblar (keyset: id > keyin) — parol hash, telefon, sirlar
  // QAYTARILMAYDI (ortiqcha maydon oshkor qilmaslik)
  r.get("/foydalanuvchilar", tekshirib({ query: Royxat },
    async ({ query }, _req, res) => {
      const { rows } = await pool.query<Record<string, unknown>>({
        text: `SELECT id, email, toliq_ism AS "toliqIsm", rol,
                 yaratilgan_vaqt AS yaratilgan,
                 email_tasdiqlangan_vaqt IS NOT NULL
                   AS "emailTasdiqlangan",
                 bloklangan_vaqt IS NOT NULL AS bloklangan
               FROM foydalanuvchilar WHERE id > $1
               ORDER BY id LIMIT $2`,
        values: [query.keyin, query.limit + 1],
        types: TURLAR,
      });
      const keyingi = rows.length > query.limit
        ? (rows[query.limit - 1]?.["id"] as number)
        : null;
      res.json({
        foydalanuvchilar: rows.slice(0, query.limit),
        keyingi,
      });
    }));

  // Bloklash: kirish taqiqlanadi va BARCHA sessiyalari darhol bekor
  r.post("/foydalanuvchilar/:id/bloklash", tekshirib({ params: Id },
    async ({ params }, req, res) => {
      const admin = req.sessiya;
      if (admin === undefined) throw new KirilmaganXatosi();
      if (params.id === admin.foydalanuvchi.id) {
        throw new IlovaXatosi(409, "O'zingizni bloklay olmaysiz", {
          kod: "ozini-bloklash",
          sarlavha: "Mumkin emas",
        });
      }
      const { rowCount } = await pool.query(
        `UPDATE foydalanuvchilar
         SET bloklangan_vaqt = coalesce(bloklangan_vaqt, now())
         WHERE id = $1 AND rol <> 'admin'`, [params.id]);
      if (rowCount !== 1) {
        throw new TopilmadiXatosi(
          "Foydalanuvchi topilmadi (yoki administrator)",
          "foydalanuvchi-topilmadi", "Topilmadi");
      }
      const bekor = await sessiyalar.hammasiniBekorQil(params.id);
      await ombor.hodisa({
        tur: "admin_bloklash",
        foydalanuvchiId: params.id,
        ip: req.ip,
        userAgent: req.get("user-agent"),
        tafsilot: {
          admin: admin.foydalanuvchi.id,
          bekorSessiyalar: bekor,
        },
      });
      res.sendStatus(204);
    }));

  r.post("/foydalanuvchilar/:id/blokdan-chiqarish",
    tekshirib({ params: Id }, async ({ params }, req, res) => {
      const { rowCount } = await pool.query(
        "UPDATE foydalanuvchilar SET bloklangan_vaqt = NULL " +
          "WHERE id = $1", [params.id]);
      if (rowCount !== 1) {
        throw new TopilmadiXatosi("Foydalanuvchi topilmadi",
          "foydalanuvchi-topilmadi", "Topilmadi");
      }
      await ombor.hodisa({
        tur: "admin_blokdan_chiqarish",
        foydalanuvchiId: params.id,
        ip: req.ip,
        userAgent: req.get("user-agent"),
        tafsilot: { admin: req.sessiya?.foydalanuvchi.id },
      });
      res.sendStatus(204);
    }));
  return r;
}

To'rt qaror:

  • r.use(rolKerak("admin")) — router darajasida. Yangi admin yo'li qo'shilsa, tekshiruv avtomatik. Har yo'lga alohida yozish — unutish xavfi (BFLA).
  • Ortiqcha maydon yo'q. Admin ro'yxatida parol hash'i, telefon, sirlar yo'q. Hatto admin ham ko'rishi shart bo'lmagan narsani ko'rmaydi. Admin boshqalarning vazifalarini ham ko'rmaydi — buning uchun yo'l yo'q.
  • O'zini va boshqa adminni bloklab bo'lmaydi. O'zini — 409. Boshqa adminni — rol <> 'admin' sharti, natija 404. Adminlar urushi bo'lmaydi.
  • Bloklash darhol ishlaydi: sessiyalar bekor, sessiya tekshiruvi esa bloklangan_vaqt IS NULL ni ham talab qiladi.

6.3 Markaziy tekshiruv: bloklangan hech qayerdan kirmaydi

Kirishning yo'li ko'p: parol, SMS, Google, Telegram, passkey, 2FA. Har biriga "bloklanganmi?" shartini qo'yish — biri albatta unutiladi. Kanon uni bitta joyga qo'ydi — sessiya beruvchi sessiyaBer funksiyasiga:

ts
async function sessiyaBer(
  req: Request, res: Response, f: Foydalanuvchi, eslabQol: boolean,
): Promise<void> {
  // bloklangan hisob — HECH QAYSI yo'l (parol, SMS, Google,
  // Telegram, passkey, 2FA) sessiya olmaydi
  if (await d.ombor.bloklanganmi(f.id)) {
    await xizmat.hodisa({
      tur: "bloklangan_kirish", foydalanuvchiId: f.id, ...mijoz(req),
    });
    throw new IlovaXatosi(403,
      "Hisob bloklangan — administrator bilan bog'laning",
      { kod: "hisob-bloklangan", sarlavha: "Hisob bloklangan" });
  }
  // ... eski sessiyani bekor qilish, yangisini yaratish, cookie
}

Bu yerda 403 to'g'ri: foydalanuvchi parolni to'g'ri kiritdi (kim ekani ma'lum), lekin kirishga ruxsat yo'q.

7. Imzolangan kursor

25-qismda sahifalash kursorini ochiq base64url(JSON) qilgan edik va "imzolash 28-qismda" degan edik (Pagination). Mijoz kursor ichini o'zgartirib, "o'zi yasagan" keyset holatini yubora olardi. Qiymatlar baribir SQL parametri — injection emas. Lekin kelajakda kursorga egasi kabi maydon qo'shilsa, bu IDOR'ga yo'l ochadi. Endi kursor HMAC bilan imzolanadi:

js
const enc = new TextEncoder();
const b64url = (b) => btoa(String.fromCharCode(...new Uint8Array(b)))
  .replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
const dan64url = (t) =>
  atob(t.replaceAll("-", "+").replaceAll("_", "/"));

const kalit = await crypto.subtle.importKey(
  "raw", enc.encode("faqat-namuna-kalit-32-bayt-uzun!"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
async function imzo(t) {
  const mac = await crypto.subtle.sign("HMAC", kalit, enc.encode(t));
  return b64url(mac.slice(0, 16));
}

async function kursorYoz(qiymatlar, sorov) {
  const json = JSON.stringify({ v: qiymatlar, i: sorov });
  const t = b64url(enc.encode(json));
  return `${t}.${await imzo(t)}`;
}

async function kursorOqi(kursor, sorov) {
  const [t, berilgan] = kursor.split(".");
  if (!t || berilgan !== (await imzo(t))) return undefined;
  const ichki = JSON.parse(dan64url(t));
  return ichki.i === sorov ? ichki.v : undefined;
}

const k = await kursorYoz([4, 9], "teg=oqish");
console.log(k);
console.log(await kursorOqi(k, "teg=oqish"));
console.log(await kursorOqi(k, "teg=oila"));

const [, imzoQismi] = k.split(".");
const soxtaIchki = b64url(enc.encode(
  JSON.stringify({ v: [0, 0], i: "teg=oqish" })));
const soxta = `${soxtaIchki}.${imzoQismi}`;
console.log(await kursorOqi(soxta, "teg=oqish"));

Konsolda:

text
eyJ2IjpbNCw5XSwiaSI6InRlZz1vcWlzaCJ9.u7S7RByFAHdP777Qc_BDlw
[4, 9]
undefined
undefined

Ikkinchi qator — to'g'ri kursor. Uchinchisi — kursor boshqa filtr (teg=oila) bilan ishlatildi: rad. To'rtinchisi — ichi o'zgartirildi, imzo eskisi: rad. HMAC'ning 16 bayti (128 bit) yetarli va kursorni qisqa tutadi. Brauzer misolida qisqalik uchun !== bilan solishtirdik; kanon kodida — timingSafeEqual. Kalit ILOVA_SIRI dan HKDF bilan olinadi (maqsadi kursor); misoldagi kalit — faqat namuna.

8. Hujumchi nigohi

Har qatorda: hujum, himoya va uni qotiradigan test.

Hujum Himoya Test
URL'dagi id ni o'zgartirish (IDOR) AND egasi_id, RLS IDOR to'plami: begona — 404
begona Idempotency-Key kalit + foydalanuvchi Dilnoza Azizning javobini olmaydi
admin yo'liga oddiy sessiya bilan rolKerak router darajasida 401 va 403 testi
403/404 farqi bilan id'larni sanash begonaga ham 404 "404 (403 emas)"
WHERE unutilgan yangi so'rov vazifalar_api + RLS rls.test.ts izolyatsiya
ma'lumot roli bilan sessiya hash'lari REVOKE, ikki rol rollar chegarasi
bloklangan hisob boshqa yo'ldan sessiyaBer da markaziy bloklash: darhol 401, kirish 403
kursor ichini o'zgartirish HMAC kursor o'zgartirilgan kursor — 400

9. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
WHERE id = $1 (egasiz) IDOR AND egasi_id = $2
begona resursga 403 id'lar mavjudligi oshkor 404
kirmaganga 403 mijoz noto'g'ri xulosa qiladi 401
rol JWT ichida, muddati uzoq adminlikdan olingan ham admin rol bazadan, har so'rovda
rol tekshiruvi har yo'lda alohida bittasi unutiladi router darajasida
API superuser bilan RLS ishlamaydi cheklangan rol
ALTER DEFAULT PRIVILEGES ni unutish yangi jadvallar ochiq REVOKE, huquqlarni ko'rib chiqish
admin javobida hamma ustun parol hash'i sizadi aniq ustunlar ro'yxati

10. Mashqlar

Mashqlar kurs/mashqlar/28/20-rbac/ da.

1-mashq (oson): qaysi kod?

Har holat uchun 401, 403 yoki 404 ni tanlang:

  1. Cookie'siz GET /v1/admin/foydalanuvchilar.
  2. Aziz (oddiy foydalanuvchi) GET /v1/admin/foydalanuvchilar.
  3. Dilnoza DELETE /v1/vazifalar/1 (Azizniki).
  4. Aziz GET /v1/vazifalar/999 (bunday id yo'q).
  5. Bloklangan Sardor to'g'ri parol bilan POST /v1/auth/kirish.
Yechim
  1. 401 — kim ekani noma'lum.
  2. 403 — kim ekani ma'lum, rol mos emas.
  3. 404 — begona resurs; 403 mavjudlikni oshkor qilardi.
  4. 404 — 3-holat bilan bir xil javob, tashqaridan farqlanmaydi.
  5. 403 hisob-bloklangan — parol to'g'ri (kimligi ma'lum), lekin kirish taqiqlangan.

2-mashq (o'rta): egalik bilan ombor

Xotiradagi kichik ombor yozing: omborYarat(egasiId) funksiyasi ol(id), yangila(id, matn) va ochir(id) usullari bor obyekt qaytarsin. Har usul faqat shu egasining vazifalari bilan ishlasin, begonasida undefined (yoki false) qaytarsin. Umumiy massiv: { id: 1, egasiId: 1, matn: "Azizniki" } va { id: 5, egasiId: 2, matn: "Dilnozaniki" }. Dilnozaning omborida 1-vazifani o'qish, yangilash va o'chirishga urining, keyin Azizning omborida 1-vazifa o'zgarmaganini chiqaring.

Yechim
js
const VAZIFALAR = [
  { id: 1, egasiId: 1, matn: "Azizniki" },
  { id: 5, egasiId: 2, matn: "Dilnozaniki" },
];

function omborYarat(egasiId) {
  const top = (id) =>
    VAZIFALAR.find((v) => v.id === id && v.egasiId === egasiId);
  return {
    ol: (id) => top(id),
    yangila(id, matn) {
      const v = top(id);
      if (v === undefined) return undefined;
      v.matn = matn;
      return v;
    },
    ochir(id) {
      const i = VAZIFALAR.indexOf(top(id));
      if (i === -1) return false;
      VAZIFALAR.splice(i, 1);
      return true;
    },
  };
}

const dilnoza = omborYarat(2);
console.log(dilnoza.ol(1));
console.log(dilnoza.yangila(1, "Buzildi"));
console.log(dilnoza.ochir(1));
console.log(dilnoza.ol(5).matn);

const aziz = omborYarat(1);
console.log(aziz.ol(1).matn, VAZIFALAR.length);

Konsolda:

text
undefined
undefined
false
Dilnozaniki
Azizniki 2

Asosiy g'oya — egasi ombor yaratilganda bir marta beriladi va hamma usul faqat top orqali ishlaydi. Yangi usul qo'shganda egalikni unutib bo'lmaydi: boshqa qidiruv yo'q. Kanon PgOmbori ham shunday: new PgOmbori(pool, egasiId). indexOf(undefined) → -1 — begona id'da o'chirish jim muvaffaqiyat bo'lmaydi.

3-mashq (qiyin): IDOR test generatori

Kichik "API" funksiyasini yozing: api(metod, yol, foydalanuvchiId) → status kod. Yo'llar: GET, PATCH, DELETE — /vazifalar/:id. Egalik tekshiruvi bo'lsin, lekin DELETE da ataylab unutilgan bo'lsin (xato). Keyin idorTest(resursId, egasi, begona) funksiyasini yozing: uchala metodni begona foydalanuvchi bilan sinab, 404 bo'lmaganlarini TESHIK deb chiqarsin. Ishora: yo'ldan id ni Number(yol.split("/")[2]) bilan oling.

Yechim
js
const VAZIFALAR = new Map([
  [1, { egasiId: 1, matn: "Azizniki" }],
  [5, { egasiId: 2, matn: "Dilnozaniki" }],
]);

function api(metod, yol, foydalanuvchiId) {
  const id = Number(yol.split("/")[2]);
  const v = VAZIFALAR.get(id);
  if (v === undefined) return 404;
  if (metod === "GET" || metod === "PATCH") {
    return v.egasiId === foydalanuvchiId ? 200 : 404;
  }
  if (metod === "DELETE") {
    VAZIFALAR.delete(id); // xato: egalik tekshirilmadi
    return 204;
  }
  return 405;
}

function idorTest(resursId, egasi, begona) {
  const yol = `/vazifalar/${resursId}`;
  for (const metod of ["GET", "PATCH", "DELETE"]) {
    const kod = api(metod, yol, begona);
    console.log(metod, kod, kod === 404 ? "ok" : "TESHIK");
  }
  console.log("egasi ko'radimi:", api("GET", yol, egasi));
}

idorTest(1, 1, 2);

Konsolda:

text
GET 404 ok
PATCH 404 ok
DELETE 204 TESHIK
egasi ko'radimi: 404

Test ikki narsani ushladi: DELETE begonaga 204 qaytardi, va oxirgi qator — egasi endi o'z vazifasini ko'rmaydi, chunki begona uni o'chirib yubordi. Shuning uchun kanon testi oxirida egasi resursini o'zgarmagan holda ko'rishini ham tekshiradi. Testni GET bilan tugatsangiz, bunday "jim" zararni sezmay qolasiz.

11. Loyiha qadami: rollar, admin va RLS ostida API

11.1 Migratsiyalar

sql/119-rollar-rls-api.sql (= prisma/migrations/20261028110000_rollar_rls_api; schema.prisma ga rol, bloklanganVaqt). Beshta qism:

  1. foydalanuvchilar.rol (CHECK (rol IN ('foydalanuvchi', 'admin')), sukut foydalanuvchi) va bloklangan_vaqt.
  2. vazifalar_auth roli (LOGIN, CONNECTION LIMIT 20): auth jadvallariga DML, foydalanuvchilar uchun o'z siyosati USING (true).
  3. REVOKE ALL sakkizta auth jadvalidan vazifalar_ilova dan.
  4. vazifa_idlari_band(bigint[]) — SECURITY DEFINER, search_path qotirilgan, EXECUTE faqat vazifalar_ilova; vazifalar_id_seq ga SELECT, UPDATE.
  5. _prisma_migrations bo'lsa — unga SELECT (migratsiya holati tekshiruvi uchun).

Funksiya qismi (kanon matni, izohsiz):

sql
CREATE FUNCTION vazifa_idlari_band(idlar bigint[]) RETURNS bigint[]
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = pg_catalog, public
RETURN (SELECT coalesce(array_agg(id), '{}')
        FROM vazifalar WHERE id = ANY(idlar));
REVOKE EXECUTE ON FUNCTION vazifa_idlari_band(bigint[]) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION vazifa_idlari_band(bigint[])
  TO vazifalar_ilova;

Parollar faylda yo'q: lokal bazada trust, serverda rollar parol bilan (scram-sha-256) va parol .env da. Namuna fayl sql/120-namuna-admin.sql — bitta qator: Bekzodni admin qiladi (faqat lokal; prodakshnda admin qo'lda tayinlanadi, migratsiyada emas).

11.2 Kod o'zgarishlari

  • src/auth/admin.ts — yuqoridagi modul; ilova.ts da v1.use("/admin", adminRouteri(...)).
  • src/auth/sessiya.ts — sessiya tekshiruvi f.rol ni oladi va f.bloklangan_vaqt IS NULL ni talab qiladi; JoriySessiya turiga rol.
  • src/auth/marshrut.ts — sessiyaBer da markaziy bloklash tekshiruvi.
  • src/vazifalar/pg-ombor.ts — hamma o'qishlar ham tranzaksiyada. Umumiy #tranzaksiyada yordamchisi BEGIN dan keyin bitta qator qo'shdi:
ts
await c.query("BEGIN");
// RLS va audit (vazifa_tarixi.kim_id) — shu tranzaksiya uchun
// joriy foydalanuvchi. SET LOCAL parametr olmaydi — set_config
await c.query(
  "SELECT set_config('ilova.foydalanuvchi_id', $1, true)",
  [String(this.#egasi)],
);

almashtirHammasi begona id'larni endi vazifa_idlari_band bilan aniqlaydi. PrismaOmbori da xuddi shu — interaktiv $transaction + $executeRaw bilan set_config (Prisma'da "har so'rov sozlamasi" yo'q).

  • src/server.ts — ikki hovuz (pool): DATABASE_URL (vazifalar_api) va AUTH_DATABASE_URL (vazifalar_auth, application_name vazifalar-api-auth). Ikkinchisi berilmasa — bitta hovuz (lokal dev).
  • src/umumiy/kursor.ts — HMAC kursor (kursorYoz, kursorOqi); kalit ILOVA_SIRI → HKDF kursor.

Bonus: 27-qismdan qolgan vazifa yopildi — vazifa_tarixi.kim_id endi yoziladi, chunki trigger set_config qiymatini o'qiydi.

Ulanish satrlari (lokal trust namunasi, parolsiz):

bash
DATABASE_URL=postgres://vazifalar_api@127.0.0.1:5432/vazifalar
AUTH_DATABASE_URL=postgres://vazifalar_auth@127.0.0.1:5432/vazifalar

Kanon .env.example da AUTH_DATABASE_URL qism oxirida paydo bo'ladi, DATABASE_URL namunasi esa hali ham postgres foydalanuvchisi bilan qolgan. O'z loyihangizda ma'lumot hovuziga vazifalar_api ni yozing — bugungi darsning butun ma'nosi shunda. Kanonning jonli tekshiruvi aynan shu ikki rol bilan ishlagan.

11.3 Testlar

test/admin.test.ts (+5) va test/rls.test.ts (+15). RLS testlari 25–27-qismlardagi ombor shartnoma testlarini endi vazifalar_api roli bilan, RLS ostida, ikkala ombor (pg va Prisma) uchun qayta yurgizadi. Biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z PostgreSQL 17.6 bazamiz):

bash
node --test test/admin.test.ts

Har qatorda status kodlarga qarang: 401, 403 va 404 qayerda chiqqani:

text
✔ HMAC kursor: o'z kaliti bilan — ok; ichi o'zgartirilsa, boshqa kalit, imzosiz (25-qism formati), boshqa so'rov imzosi — undefined (4.7351ms)
✔ 401 (sessiyasiz) va 403 (oddiy foydalanuvchi); admin ro'yxati — parol hash, telefon YO'Q; keyset sahifalash (1081.2746ms)
✔ bloklash: sessiyasi DARHOL 401, parol bilan kirish 403 hisob-bloklangan; o'zini va boshqa adminni — yo'q; ochilgach — kiradi (1531.0287ms)
✔ rol har so'rovda bazadan: adminlikdan olinsa — o'sha sessiya bilan darhol 403 (1267.6166ms)
✔ API'dagi kursor imzolangan: o'zgartirilgan kursor — 400 (2474.9637ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 9186.0024

Nima bo'ldi? Beshala test o'tdi. Kirmaganga 401, oddiy foydalanuvchiga 403. Bloklangan hisob darhol chiqariladi. Rol olib tashlansa, o'sha sessiya ham 403 oladi. Soxta kursor — 400. rls.test.ts ham o'z bazamizda 15/15 o'tdi. Unda: ikki ombor shartnomasi (har biri 5 test), izolyatsiya, PUT begona id bilan, kim_id, rollar chegarasi va ikki rol bilan to'liq HTTP oqim (begona id — 404).

Kanon tekshiruvi (2026-10-28, PostgreSQL 18.6): tip va lint xatosiz, testlar 204 (203 + 1 o'tkazilgan), Testcontainers 10, e2e 6/6, 12 migratsiya sql/ bilan bir xil. Jonli API endi haqiqiy rollar bilan ishlaydi: pg_stat_activity da vazifalar-api | vazifalar_api va vazifalar-api-auth | vazifalar_auth.

11.4 Commit

Tip, lint va testlar o'tgach — commit. Keyingi dars (ABAC) faqat tajriba, loyihaga hech narsa qo'shmaydi, shuning uchun holat shu yerda yopiladi:

bash
M="feat(auth): RBAC (rol, admin yo'llari, bloklash), API ikki rol"
M+=" bilan — vazifalar_api (RLS, set_config) va vazifalar_auth;"
M+=" HMAC kursor; audit kim_id; db: 119 (+120 namuna)"
git add src test sql prisma
git commit -m "$M"

12. Real ishda

  • Multi-tenant tizimlarda (bitta ilova, ko'p kompaniya) egalik ikki qavatli: foydalanuvchi va tashkilot. Har so'rovda ikkalasi ham tekshiriladi.
  • Freymvorklar rol tekshiruvini dekorator yoki "guard" bilan beradi (NestJS — 30-qismda). Lekin obyekt darajasidagi egalikni hech qaysi freymvork siz uchun yozmaydi.
  • Supabase kabi platformalar frontend'dan bazaga to'g'ridan-to'g'ri so'rov yuboradi — u yerda RLS yagona himoya, shuning uchun siyosatlar juda puxta yoziladi.
  • Intervyuda: "401 va 403 farqi?", "IDOR nima va uni qanday topasiz?", "Begona resursga nega 404?", "RLS'ni ilova bilan qanday ulaysiz?".

Xulosa

  • Avtorizatsiya ikki daraja: funksiya (rol — RBAC) va obyekt (egalik).
  • 401 — kim noma'lum; 403 — rol mos emas; begona resurs — 404 (mavjudlik oshkor bo'lmasin).
  • IDOR — eng ko'p uchraydigan API zaifligi; himoyasi AND egasi_id va har resurs uchun "begona — 404" testi.
  • Rol bazadan, har so'rovda; tekshiruv router darajasida; bloklash — sessiyaBer da markaziy.
  • API superuser bo'lsa RLS ishlamaydi: ikki cheklangan rol, har tranzaksiyada set_config, default privileges'ni REVOKE.
  • Kursor HMAC bilan imzolanadi — mijoz uning ichini o'zgartira olmaydi.

Keyingi dars: ABAC, ReBAC va policy engine'lar — "faqat ish vaqtida", "ulashilgan vazifani faqat o'qish" kabi murakkab qoidalarni rol bilan ifodalab bo'lmaganda.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
RBAC va resurs egaligi: rollar, 401 va 403, IDOR testlari va RLS — IlmHamroh