Tarkibga o'tish

RBAC

RBAC (Role-Based Access Control) — rollarga asoslangan kirish nazorati. Bu modelda ruxsatlar foydalanuvchilarga emas, rollarga beriladi. Foydalanuvchi esa bir yoki bir nechta rolga ega bo‘ladi.

Maktabni misol qilib olamiz. "O‘qituvchi" jurnalga baho qo‘ya oladi, "o‘quvchi" faqat o‘z bahosini ko‘radi, "direktor" esa barcha jurnallarni ko‘radi. Maktabga yangi o‘qituvchi kelganda unga bittadan huquq berib chiqilmaydi. Uni shunchaki "o‘qituvchi" deb belgilash kifoya. RBAC ham xuddi shunday ishlaydi.

Uch qatlam

Foydalanuvchi ──→ Rol ──→ Ruxsatlar

Aziza   ──→ author ──→ books:read, books:create, books:update
Bobur   ──→ reader ──→ books:read
Dilnoza ──→ admin  ──→ books:*, users:*
  • Ruxsat (permission) — bitta aniq amal: books:create, users:delete.
  • Rol (role) — ruxsatlar to‘plami: reader, author, admin.
  • Foydalanuvchi — rolga ega bo‘lgan subyekt.

Nima uchun kod rolni emas, ruxsatni tekshirishi kerak? Quyidagi ikki variantni solishtiring:

// ❌ Rol tekshiriladi
if p.Role == "admin" || p.Role == "author" { ... }

// ✅ Ruxsat tekshiriladi
if Can(p.Role, PermBooksCreate) { ... }

Yangi editor roli qo‘shilsa, birinchi variantda barcha iflarni topib, yangilab chiqish kerak bo‘ladi. Ikkinchisida esa faqat rol ta’rifiga bitta qator qo‘shiladi. Handlerlar o‘zgarmaydi.

Ruxsat nomlash

Keng tarqalgan format — resurs:amal:

books:read      books:create     books:update     books:delete
users:read      users:update     users:delete
reports:read

Nomlar kodda konstanta sifatida e’lon qilinadi. Shunda xato yozilgan nom kompilyatsiya paytidayoq aniqlanadi:

type Permission string

const (
    PermBooksRead   Permission = "books:read"
    PermBooksCreate Permission = "books:create"
    PermBooksUpdate Permission = "books:update"
    PermBooksDelete Permission = "books:delete"
    PermUsersRead   Permission = "users:read"
    PermUsersManage Permission = "users:manage"
)

Permission alohida tur sifatida e’lon qilindi. Endi Can(role, "books:craete") kabi xato satrni tasodifan berib bo‘lmaydi.

Rollarni e’lon qilish

type Role string

const (
    RoleReader Role = "reader"
    RoleAuthor Role = "author"
    RoleAdmin  Role = "admin"
)

var rolePermissions = map[Role][]Permission{
    RoleReader: {PermBooksRead},
    RoleAuthor: {PermBooksRead, PermBooksCreate, PermBooksUpdate},
    RoleAdmin: {
        PermBooksRead, PermBooksCreate, PermBooksUpdate, PermBooksDelete,
        PermUsersRead, PermUsersManage,
    },
}

Barcha qoidalar bitta joyda. "Muallif nima qila oladi?" degan savolga javob topish uchun butun loyihani qidirish shart emas.

Tekshirish funksiyasi

Ruxsatni tez tekshirish uchun ro‘yxat dastur ishga tushganda to‘plam (set) ko‘rinishiga keltiriladi:

var grants = buildGrants(rolePermissions)

func buildGrants(src map[Role][]Permission) map[Role]map[Permission]struct{} {
    out := make(map[Role]map[Permission]struct{}, len(src))
    for role, perms := range src {
        set := make(map[Permission]struct{}, len(perms))
        for _, p := range perms {
            set[p] = struct{}{}
        }
        out[role] = set
    }
    return out
}

func Can(role Role, perm Permission) bool {
    _, ok := grants[role][perm]
    return ok
}

Can noma’lum rol uchun ham xavfsiz ishlaydi. grants["hacker"] nil map qaytaradi, nil map’dan o‘qish esa ok == false beradi. Demak, noma’lum rol hech qanday ruxsat olmaydi. Bu sukut bo‘yicha rad etish tamoyilining amaldagi ko‘rinishi.

map[Permission]struct{} — Go’da to‘plam yaratishning odatiy usuli. struct{} xotirada joy egallamaydi.

To‘liq misol

main.go
package main

import (
    "fmt"
    "slices"
)

type Permission string

const (
    PermBooksRead   Permission = "books:read"
    PermBooksCreate Permission = "books:create"
    PermBooksUpdate Permission = "books:update"
    PermBooksDelete Permission = "books:delete"
    PermUsersRead   Permission = "users:read"
    PermUsersManage Permission = "users:manage"
)

type Role string

const (
    RoleReader Role = "reader"
    RoleAuthor Role = "author"
    RoleAdmin  Role = "admin"
)

var rolePermissions = map[Role][]Permission{
    RoleReader: {PermBooksRead},
    RoleAuthor: {PermBooksRead, PermBooksCreate, PermBooksUpdate},
    RoleAdmin: {
        PermBooksRead, PermBooksCreate, PermBooksUpdate, PermBooksDelete,
        PermUsersRead, PermUsersManage,
    },
}

var grants = buildGrants(rolePermissions)

func buildGrants(src map[Role][]Permission) map[Role]map[Permission]struct{} {
    out := make(map[Role]map[Permission]struct{}, len(src))
    for role, perms := range src {
        set := make(map[Permission]struct{}, len(perms))
        for _, p := range perms {
            set[p] = struct{}{}
        }
        out[role] = set
    }
    return out
}

func Can(role Role, perm Permission) bool {
    _, ok := grants[role][perm]
    return ok
}

// CanAny foydalanuvchida bir nechta rol bo‘lgan holat uchun.
func CanAny(roles []Role, perm Permission) bool {
    return slices.ContainsFunc(roles, func(r Role) bool { return Can(r, perm) })
}

func main() {
    checks := []struct {
        role Role
        perm Permission
    }{
        {RoleReader, PermBooksRead},
        {RoleReader, PermBooksCreate},
        {RoleAuthor, PermBooksCreate},
        {RoleAuthor, PermBooksDelete},
        {RoleAdmin, PermBooksDelete},
        {"hacker", PermBooksRead},
        {"", PermBooksRead},
    }

    for _, c := range checks {
        result := "❌ rad"
        if Can(c.role, c.perm) {
            result = "✅ ruxsat"
        }
        fmt.Printf("%-8q %-14s %s\n", c.role, c.perm, result)
    }

    fmt.Println("reader+author, books:create:", CanAny([]Role{RoleReader, RoleAuthor}, PermBooksCreate))
}
"reader" books:read     ✅ ruxsat
"reader" books:create   ❌ rad
"author" books:create   ✅ ruxsat
"author" books:delete   ❌ rad
"admin"  books:delete   ✅ ruxsat
"hacker" books:read     ❌ rad
""       books:read     ❌ rad
reader+author, books:create: true

Noma’lum hacker roli va bo‘sh rol hech qanday ruxsat olmadi.

Handlerda ishlatish

Auth middleware yozish darsidagi Principal turida foydalanuvchi roli bor. Endi uni ruxsat bilan birga tekshiramiz:

func (h *Handler) deleteBook(w http.ResponseWriter, r *http.Request) {
    p, ok := PrincipalFrom(r.Context())
    if !ok {
        writeError(w, http.StatusUnauthorized, "unauthorized", "tizimga kiring")
        return
    }
    if !Can(Role(p.Role), PermBooksDelete) {
        writeError(w, http.StatusForbidden, "forbidden", "bu amal uchun ruxsat yo‘q")
        return
    }

    // ... kitobni o‘chirish
}

Bu tekshiruvni har bir handlerga yozish ham takrorlanishga olib keladi. Middleware orqali ruxsat tekshirish darsida uni middleware’ga chiqaramiz.

Rollarni qayerda saqlash kerak?

Oddiy holat: bitta rol

Har bir foydalanuvchida faqat bitta rol bo‘lsa, users jadvalidagi bitta ustun yetarli:

ALTER TABLE users
    ADD COLUMN role TEXT NOT NULL DEFAULT 'reader'
    CHECK (role IN ('reader', 'author', 'admin'));

CHECK cheklovi bazaga noma’lum rol yozilishining oldini oladi.

Murakkab holat: bir nechta rol

Foydalanuvchi bir vaqtda bir nechta rolga ega bo‘lishi mumkin bo‘lsa, alohida jadval kerak:

CREATE TABLE user_roles (
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role    TEXT   NOT NULL,
    PRIMARY KEY (user_id, role)
);

Rollar va ruxsatlar ro‘yxatini ham bazada saqlash mumkin. Bunda administrator yangi rollarni kodni o‘zgartirmasdan, admin panel orqali yaratadi. Lekin bu murakkablikni oshiradi. Rollar kam va kamdan-kam o‘zgaradigan bo‘lsa, ularni kodda saqlash sodda va ishonchliroq: har bir o‘zgarish code review’dan o‘tadi va Git tarixida qoladi.

Tokendagi eskirgan rol muammosi

JWT darsida rol tokenning ichiga yozilgan edi. Bu qulay, chunki har bir so‘rovda bazaga murojaat qilinmaydi. Lekin bitta muammo bor:

10:00  Dilnoza admin sifatida login qiladi → token: {role: "admin", exp: 10:15}
10:05  Uni ishdan bo‘shatishdi, roli reader'ga tushirildi
10:05–10:15  Dilnozaning tokenida hali ham "admin" yozilgan

Token muddati tugaguncha eski rol amal qiladi. Bu muammoni hal qilishning uch yo‘li bor:

  1. Qisqa muddatli token. 15 daqiqalik access token xavfli oynani 15 daqiqa bilan cheklaydi. Aksariyat ilovalar uchun bu maqbul.
  2. Muhim amallarda rolni bazadan o‘qish. Foydalanuvchilarni boshqarish yoki to‘lov kabi amallarda tokendagi rolga emas, bazadagi joriy rolga qaraladi.
  3. Tokenga rol yozmaslik. Har bir so‘rovda rol bazadan yoki keshdan o‘qiladi. Bu eng aniq usul, lekin har bir so‘rov qo‘shimcha murojaat talab qiladi.

Ko‘p loyihalarda birinchi va ikkinchi usul birgalikda qo‘llanadi.

Xulosa

  • RBAC’da ruxsatlar rollarga, rollar esa foydalanuvchilarga beriladi.
  • Kod rolni emas, ruxsatni tekshiradi. Shunda yangi rol qo‘shish handlerlarni o‘zgartirmaydi.
  • Ruxsat va rollar alohida turlar va konstantalar sifatida e’lon qilinadi.
  • Barcha qoidalar bitta joyda saqlanadi. Noma’lum rol hech qanday ruxsat olmaydi.
  • Tokendagi rol eskirishi mumkin. Muhim amallarda uni bazadan tekshiring.

RBAC "kitob o‘chirish mumkinmi?" degan savolga javob beradi. Lekin u "aynan shu kitobni o‘chirish mumkinmi?" degan savolga javob bermaydi. Keyingi darsda resursga egalikni tekshirishni o‘rganamiz.