Tarkibga o'tish

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()
);
type Book struct {
    ID        int64
    Title     string
    OwnerID   int64
    Published bool
}

OwnerID resurs yaratilganda tokendan olinadi, hech qachon so‘rov tanasidan emas:

book := Book{
    Title:   req.Title,
    OwnerID: principal.UserID, // req.OwnerID 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:update ruxsati — "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
package main

import (
    "context"
    "errors"
    "fmt"
    "sync"
)

// ---------- RBAC ----------

type Permission string

const (
    PermBooksCreate    Permission = "books:create"
    PermBooksUpdate    Permission = "books:update"
    PermBooksManageAll Permission = "books:manage_all"
)

var grants = map[string]map[Permission]bool{
    "reader": {},
    "author": {PermBooksCreate: true, PermBooksUpdate: true},
    "admin":  {PermBooksCreate: true, PermBooksUpdate: true, PermBooksManageAll: true},
}

func Can(role string, p Permission) bool { return grants[role][p] }

// ---------- Model va siyosat ----------

type Principal struct {
    UserID int64
    Role   string
}

type Book struct {
    ID        int64
    Title     string
    OwnerID   int64
    Published bool
}

func isOwner(p Principal, b Book) bool { return p.UserID != 0 && p.UserID == b.OwnerID }

func CanViewBook(p Principal, b Book) bool {
    return b.Published || isOwner(p, b) || Can(p.Role, PermBooksManageAll)
}

func CanEditBook(p Principal, b Book) bool {
    if Can(p.Role, PermBooksManageAll) {
        return true
    }
    return isOwner(p, b) && Can(p.Role, PermBooksUpdate)
}

func CanDeleteBook(p Principal, b Book) bool {
    if Can(p.Role, PermBooksManageAll) {
        return true
    }
    return isOwner(p, b) && !b.Published
}

// ---------- Service ----------

var (
    ErrNotFound  = errors.New("kitob topilmadi")
    ErrForbidden = errors.New("bu amal uchun ruxsat yo‘q")
)

type BookService struct {
    mu    sync.Mutex
    books map[int64]Book
}

func (s *BookService) get(p Principal, id int64) (Book, error) {
    b, ok := s.books[id]
    if !ok || !CanViewBook(p, b) {
        return Book{}, ErrNotFound
    }
    return b, nil
}

func (s *BookService) Rename(ctx context.Context, p Principal, id int64, title string) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    b, err := s.get(p, id)
    if err != nil {
        return err
    }
    if !CanEditBook(p, b) {
        return ErrForbidden
    }
    b.Title = title
    s.books[id] = b
    return nil
}

func (s *BookService) Delete(ctx context.Context, p Principal, id int64) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    b, err := s.get(p, id)
    if err != nil {
        return err
    }
    if !CanDeleteBook(p, b) {
        return ErrForbidden
    }
    delete(s.books, id)
    return nil
}

func result(err error) string {
    if err == nil {
        return "✅ bajarildi"
    }
    return "❌ " + err.Error()
}

func main() {
    ctx := context.Background()
    svc := &BookService{books: map[int64]Book{
        1: {ID: 1, Title: "Azizaning nashr qilingan kitobi", OwnerID: 7, Published: true},
        2: {ID: 2, Title: "Azizaning qoralamasi", OwnerID: 7},
        3: {ID: 3, Title: "Boburning qoralamasi", OwnerID: 9},
    }}

    aziza := Principal{UserID: 7, Role: "author"}
    bobur := Principal{UserID: 9, Role: "author"}
    admin := Principal{UserID: 1, Role: "admin"}

    fmt.Println("Aziza 2-kitobni tahrirlaydi:  ", result(svc.Rename(ctx, aziza, 2, "Yangi nom")))
    fmt.Println("Bobur 1-kitobni tahrirlaydi:  ", result(svc.Rename(ctx, bobur, 1, "Buzildi")))
    fmt.Println("Bobur 2-kitobni tahrirlaydi:  ", result(svc.Rename(ctx, bobur, 2, "Buzildi")))
    fmt.Println("Aziza 1-kitobni o‘chiradi:    ", result(svc.Delete(ctx, aziza, 1)))
    fmt.Println("Aziza 2-kitobni o‘chiradi:    ", result(svc.Delete(ctx, aziza, 2)))
    fmt.Println("Admin 3-kitobni o‘chiradi:    ", result(svc.Delete(ctx, admin, 3)))
    fmt.Println("Bobur 99-kitobni o‘chiradi:   ", result(svc.Delete(ctx, bobur, 99)))
}
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_all ruxsati 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 olmaydiganiga 403 qaytariladi.
  • Ro‘yxatlar baza darajasida filtrlanadi.

Keyingi darsda endpoint darajasidagi ruxsatlarni middleware yordamida tekshiramiz.