Resource-based permission modeli
RBAC darsida author roli books:update ruxsatiga ega bo‘ldi. Demak, har bir muallif kitobni tahrirlay oladi. Lekin qaysi kitobni? Faqat o‘zinikini yoki hammanikini?
RBAC bu savolga javob bermaydi. U faqat amal turini biladi: "kitob tahrirlash". Aniq resurs haqida esa hech narsa bilmaydi: "Azizaning 42-kitobini tahrirlash".
Resursga asoslangan ruxsat (resource-based permission) bu bo‘shliqni to‘ldiradi. Qaror foydalanuvchi va aniq resurs orasidagi munosabatga qarab qabul qilinadi. Eng keng tarqalgan munosabat — egalik: foydalanuvchi o‘zi yaratgan resursni boshqara oladi.
Zaiflik: IDOR
Egalik tekshirilmasa, eng ko‘p uchraydigan zaifliklardan biri paydo bo‘ladi — IDOR (Insecure Direct Object Reference), ya’ni obyektga xavfsiz bo‘lmagan to‘g‘ridan-to‘g‘ri murojaat.
// ❌ IDOR: faqat token tekshirilgan, egalik esa tekshirilmagan
func (h *Handler) deleteOrder(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
h.orders.Delete(r.Context(), id)
w.WriteHeader(http.StatusNoContent)
}
Aziza tizimga kirdi va o‘z buyurtmasini o‘chirdi: DELETE /orders/1001. Keyin qiziqib, DELETE /orders/1002 yubordi. Bu Boburning buyurtmasi, lekin u ham o‘chdi. Token to‘g‘ri, rol ham to‘g‘ri. Faqat bitta savol berilmadi: "bu buyurtma kimniki?"
IDOR’ni topish uchun maxsus vosita kerak emas. URL’dagi raqamni o‘zgartirishning o‘zi yetarli. Shuning uchun u haqiqiy tizimlarda juda ko‘p uchraydi.
Egalik tekshiruvi
Resursda uning egasini ko‘rsatuvchi maydon bo‘lishi kerak:
CREATE TABLE books (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
owner_id BIGINT NOT NULL REFERENCES users(id),
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
OwnerID resurs yaratilganda tokendan olinadi, hech qachon so‘rov tanasidan emas:
Siyosat funksiyalari
Ruxsat qoidalari siyosat (policy) funksiyalarida jamlanadi. Har bir funksiya subyekt va resursni oladi va bool qaytaradi:
// CanViewBook: nashr qilingan kitobni hamma ko‘radi,
// qoralamani faqat egasi va admin ko‘radi.
func CanViewBook(p Principal, b Book) bool {
return b.Published || isOwner(p, b) || Can(p.Role, PermBooksManageAll)
}
// CanEditBook: egasi (books:update ruxsati bilan) yoki admin.
func CanEditBook(p Principal, b Book) bool {
if Can(p.Role, PermBooksManageAll) {
return true
}
return isOwner(p, b) && Can(p.Role, PermBooksUpdate)
}
// CanDeleteBook: faqat nashr qilinmagan o‘z kitobini yoki admin.
func CanDeleteBook(p Principal, b Book) bool {
if Can(p.Role, PermBooksManageAll) {
return true
}
return isOwner(p, b) && !b.Published
}
func isOwner(p Principal, b Book) bool {
return p.UserID != 0 && p.UserID == b.OwnerID
}
Bu yerda RBAC va egalik birgalikda ishlaydi:
books:updateruxsati — "umuman kitob tahrirlay oladimi?";isOwner— "bu kitob unikimi?";books:manage_all— administratorlar uchun "istalgan kitobni boshqarish" ruxsati.
isOwner funksiyasidagi p.UserID != 0 shartiga e’tibor bering. Mehmon foydalanuvchida ID 0 bo‘ladi. Agar biror kitobda ham OwnerID noto‘g‘ri tarzda 0 bo‘lib qolsa, bu shart mehmonning o‘zini kitob egasi deb hisoblashiga yo‘l qo‘ymaydi.
Siyosat funksiyalari sof funksiyalar: ular bazaga murojaat qilmaydi va tashqi holatga bog‘liq emas. Shuning uchun ularni test qilish juda oson.
Tekshiruv service qatlamida
Egalikni tekshirish uchun avval resursni bazadan o‘qish kerak. Shu sababli bu tekshiruv middleware’da emas, service’da bajariladi:
func (s *BookService) Update(ctx context.Context, p Principal, id int64, title string) (Book, error) {
book, err := s.repo.Get(ctx, id)
if err != nil {
return Book{}, err // ErrNotFound
}
if !CanViewBook(p, book) {
return Book{}, ErrNotFound // mavjudligini ham oshkor qilmaymiz
}
if !CanEditBook(p, book) {
return Book{}, ErrForbidden
}
book.Title = title
return s.repo.Update(ctx, book)
}
Service Principalni parametr sifatida oladi. U HTTP haqida hech narsa bilmaydi, lekin "kim bu amalni bajaryapti?" degan savolga javob beradi.
403 yoki 404?
Foydalanuvchi boshqa odamning kitobini ko‘rmoqchi bo‘lsa, qaysi status qaytariladi?
403 Forbidden— "bu kitob bor, lekin sizga ruxsat yo‘q". Resurs mavjudligi oshkor bo‘ladi.404 Not Found— "bunday kitob yo‘q". Resurs mavjudligi yashiriladi.
Qoida:
- foydalanuvchi resursni ko‘ra olmasa —
404. U bunday resurs borligini bilmasligi kerak; - foydalanuvchi resursni ko‘ra oladi, lekin o‘zgartira olmasa —
403. U resursni allaqachon ko‘rgan, uni yashirishning ma’nosi yo‘q.
Masalan, GitHub yopiq repozitoriyga ruxsatsiz kirilganda 404 qaytaradi. Ochiq repozitoriyga push qilishga urinilganda esa 403 qaytaradi.
So‘rovning o‘zini cheklash
Ro‘yxat endpointlarida egalik tekshiruvi so‘rov darajasida bajariladi:
// ❌ Barcha qoralamalarni yuklab, keyin Go'da filtrlash
books, _ := repo.ListAll(ctx)
for _, b := range books {
if CanViewBook(p, b) { ... }
}
// ✅ Baza faqat ruxsat etilganlarni qaytaradi
rows, err := pool.Query(ctx,
`SELECT id, title, owner_id, published FROM books
WHERE published OR owner_id = $1
ORDER BY id`, p.UserID)
Birinchi usulda million qator yuklanib, xotirada filtrlanadi. Bundan tashqari, sahifalash ham noto‘g‘ri ishlaydi. Ikkinchisida baza faqat kerakli qatorlarni qaytaradi.
Yangilash va o‘chirishda egalikni so‘rov ichida tekshirish ham xavfsiz usul:
tag, err := pool.Exec(ctx,
`DELETE FROM books WHERE id = $1 AND owner_id = $2 AND NOT published`,
id, p.UserID)
if tag.RowsAffected() == 0 {
// topilmadi yoki ruxsat yo‘q
}
Bu usul tekshiruv va o‘zgartirishni bitta amalda bajaradi, shuning uchun ular orasida holat o‘zgarib qolishi mumkin emas. Lekin 0 qator natijasidan "topilmadi" va "ruxsat yo‘q" holatlarini ajratib bo‘lmaydi. Aniq xato xabari kerak bo‘lsa, avvalgi usul, ya’ni o‘qish, tekshirish va keyin yozish ishlatiladi.
To‘liq misol
| main.go | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
Aziza 2-kitobni tahrirlaydi: ✅ bajarildi
Bobur 1-kitobni tahrirlaydi: ❌ bu amal uchun ruxsat yo‘q
Bobur 2-kitobni tahrirlaydi: ❌ kitob topilmadi
Aziza 1-kitobni o‘chiradi: ❌ bu amal uchun ruxsat yo‘q
Aziza 2-kitobni o‘chiradi: ✅ bajarildi
Admin 3-kitobni o‘chiradi: ✅ bajarildi
Bobur 99-kitobni o‘chiradi: ❌ kitob topilmadi
Natijani tahlil qilamiz:
- Bobur 1-kitobni ko‘ra oladi, chunki u nashr qilingan. Lekin u kitob egasi emas, shuning uchun
ErrForbidden→403. - Bobur 2-kitobni ko‘ra olmaydi, chunki bu boshqa odamning qoralamasi. Natija —
ErrNotFound→404. Bobur bu kitob mavjudligini bilmaydi. Uning javobi mavjud bo‘lmagan 99-kitob uchun olingan javobdan farq qilmaydi. - Aziza 1-kitobni o‘chira olmaydi, chunki u nashr qilingan. Egalik bor, lekin biznes qoidasi bunga yo‘l qo‘ymaydi.
- Admin
books:manage_allruxsati tufayli boshqa odamning qoralamasini ham o‘chira oldi.
Siyosatlarni test qilish
Siyosat funksiyalari sof bo‘lgani uchun ularni jadvalli testlar bilan to‘liq qamrab olish oson:
func TestCanEditBook(t *testing.T) {
book := Book{ID: 1, OwnerID: 7}
tests := []struct {
name string
p Principal
want bool
}{
{"egasi, author", Principal{UserID: 7, Role: "author"}, true},
{"egasi, reader", Principal{UserID: 7, Role: "reader"}, false},
{"boshqa author", Principal{UserID: 9, Role: "author"}, false},
{"admin", Principal{UserID: 1, Role: "admin"}, true},
{"mehmon", Principal{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CanEditBook(tt.p, book); got != tt.want {
t.Errorf("CanEditBook() = %v, kutilgan %v", got, tt.want)
}
})
}
}
Avtorizatsiya qoidalari — testlar eng ko‘p foyda keltiradigan joylardan biri. Bitta qoida noto‘g‘ri yozilsa, u butun tizimdagi ma’lumotlarni ochib qo‘yishi mumkin.
Xulosa
- RBAC amal turini, resursga asoslangan ruxsat esa aniq resursni tekshiradi. Ular birgalikda ishlatiladi.
- Egalik tekshirilmasa, IDOR zaifligi paydo bo‘ladi: URL’dagi ID’ni o‘zgartirib, begona ma’lumotga kirish mumkin bo‘lib qoladi.
- Resurs egasi tokendan olinadi, so‘rov tanasidan emas.
- Qoidalar sof siyosat funksiyalarida jamlanadi va service qatlamida chaqiriladi.
- Resursni ko‘ra olmagan foydalanuvchiga
404, ko‘ra oladigan, lekin o‘zgartira olmaydiganiga403qaytariladi. - Ro‘yxatlar baza darajasida filtrlanadi.
Keyingi darsda endpoint darajasidagi ruxsatlarni middleware yordamida tekshiramiz.