Tarkibga o'tish

Middleware orqali ruxsat tekshirish

RBAC darsida ruxsat tekshiruvi handler ichida yozilgan edi:

func (h *Handler) deleteBook(w http.ResponseWriter, r *http.Request) {
    p, ok := PrincipalFrom(r.Context())
    if !ok {
        writeError(w, http.StatusUnauthorized, ...)
        return
    }
    if !Can(Role(p.Role), PermBooksDelete) {
        writeError(w, http.StatusForbidden, ...)
        return
    }
    // ... asosiy ish
}

O‘nta handlerda bu kod o‘n marta takrorlanadi. Bundan ham yomoni, handlerning boshida uni yozishni unutish oson. Kod ishlaydi, testlar o‘tadi, endpoint esa hammaga ochiq qoladi.

Bu darsda endpoint darajasidagi ruxsat tekshiruvini middleware’ga chiqaramiz. Natijada har bir endpointning himoyasi route ro‘yxatida, bir qarashda ko‘rinib turadi.

RequirePermission middleware

func RequirePermission(perm Permission) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(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), perm) {
                writeError(w, http.StatusForbidden, "forbidden", "bu amal uchun ruxsat yo‘q")
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Middleware parametr qabul qiladi, ya’ni qaysi ruxsat kerakligini. Shu sababli u middleware qaytaradigan funksiya ko‘rinishida yozilgan. Bu naqsh Middleware va logging asoslari darsida tushuntirilgan.

PrincipalFrom false qaytarsa, 401 beriladi. Bu holat odatda auth middleware ulanmay qolganini bildiradi. Lekin xavfsiz tomonga og‘ish kerak: foydalanuvchi noma’lum bo‘lsa, ruxsat berilmaydi.

Auth va ruxsat zanjiri

Ruxsat middleware’i auth middleware’dan keyin ishlashi kerak. Chunki u auth middleware contextga joylagan Principaldan foydalanadi:

so‘rov → Authenticate → RequirePermission(books:delete) → handler
            │                    │
            401                  403

Buni har bir route uchun qo‘lda yozish noqulay:

mux.Handle("DELETE /books/{id}",
    auth.Require(RequirePermission(PermBooksDelete)(http.HandlerFunc(h.deleteBook))))

Kichik yordamchi funksiya bu yozuvni ancha soddalashtiradi:

// protect handlerni autentifikatsiya va berilgan ruxsat tekshiruvi bilan o‘raydi.
func (a *Auth) protect(perm Permission, h http.HandlerFunc) http.Handler {
    return a.Require(RequirePermission(perm)(h))
}

Endi route ro‘yxati o‘qish uchun qulay:

mux.HandleFunc("GET /books", h.listBooks)                               // ochiq
mux.Handle("POST /books", auth.protect(PermBooksCreate, h.createBook))
mux.Handle("PUT /books/{id}", auth.protect(PermBooksUpdate, h.updateBook))
mux.Handle("DELETE /books/{id}", auth.protect(PermBooksDelete, h.deleteBook))
mux.Handle("GET /admin/users", auth.protect(PermUsersRead, h.listUsers))

Kodni ko‘rib chiqayotgan dasturchi (code reviewer) har bir endpoint qanday himoyalanganini darhol ko‘radi. Himoyasiz qolgan endpoint esa ko‘zga tashlanib turadi.

RequireRole — qachon kerak?

Ba’zan ruxsat emas, rolning o‘zini tekshirish qulayroq. Masalan, butun admin paneli uchun:

func RequireRole(roles ...Role) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            p, ok := PrincipalFrom(r.Context())
            if !ok {
                writeError(w, http.StatusUnauthorized, "unauthorized", "tizimga kiring")
                return
            }
            if !slices.Contains(roles, Role(p.Role)) {
                writeError(w, http.StatusForbidden, "forbidden", "bu bo‘lim uchun ruxsat yo‘q")
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Lekin RBAC darsida aytilganidek, iloji boricha ruxsatni tekshirgan ma’qul. RequireRole faqat haqiqatan ham rolga bog‘liq holatlarda ishlatiladi. Masalan, "faqat admin rolidagilar admin paneliga kira oladi".

Endpointlarni guruhlash

Admin panelida o‘nlab endpoint bo‘lishi mumkin. Har biriga alohida himoya qo‘yish o‘rniga, ularni alohida mux’ga yig‘ib, butun guruhni bitta middleware bilan o‘rash mumkin:

admin := http.NewServeMux()
admin.HandleFunc("GET /admin/users", h.listUsers)
admin.HandleFunc("DELETE /admin/users/{id}", h.deleteUser)
admin.HandleFunc("GET /admin/stats", h.stats)

mux.Handle("/admin/", auth.Require(RequireRole(RoleAdmin)(admin)))
  • Asosiy mux’dagi /admin/ patterni /admin/ bilan boshlanadigan barcha so‘rovlarni ushlaydi.
  • So‘rov avval auth, keyin rol tekshiruvidan o‘tadi va ichki admin mux’iga yetib boradi.
  • Ichki mux’dagi patternlar to‘liq yo‘l bilan yozilgan: /admin/users, /users emas. Chunki so‘rov yo‘li o‘zgarmasdan uzatiladi.
  • Admin guruhiga yangi endpoint qo‘shilsa, u avtomatik himoyalangan bo‘ladi. Himoyani unutish imkoni yo‘q.

gorilla/mux va Gin kabi routerlarda guruhlash uchun maxsus imkoniyatlar bor. Ular Subroutes va Gin arxitekturasi darslarida ko‘riladi.

To‘liq misol

Misolni o‘zi ichida sinab ko‘rish uchun net/http/httptest paketi ishlatiladi. U haqiqiy server ishga tushirmasdan, router’ga so‘rov yuborib, javobni tekshirish imkonini beradi.

JWT tekshiruvi Auth middleware yozish darsida to‘liq ko‘rilgan. Bu yerda diqqatni ruxsatlarga qaratish uchun uning o‘rniga soddalashtirilgan auth middleware ishlatiladi. U tokenni tayyor ro‘yxatdan qidiradi.

main.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "slices"
    "strings"
)

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

type Permission string
type Role string

const (
    PermBooksCreate Permission = "books:create"
    PermBooksDelete Permission = "books:delete"

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

var grants = map[Role]map[Permission]bool{
    RoleReader: {},
    RoleAuthor: {PermBooksCreate: true},
    RoleAdmin:  {PermBooksCreate: true, PermBooksDelete: true},
}

func Can(r Role, p Permission) bool { return grants[r][p] }

// ---------- Principal va context ----------

type Principal struct {
    UserID int64
    Role   Role
}

type principalKey struct{}

func PrincipalFrom(ctx context.Context) (Principal, bool) {
    p, ok := ctx.Value(principalKey{}).(Principal)
    return p, ok
}

// ---------- Yordamchilar ----------

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, code, msg string) {
    writeJSON(w, status, map[string]any{"error": map[string]string{"code": code, "message": msg}})
}

// ---------- Middleware'lar ----------

// Auth — namuna uchun soddalashtirilgan. Real loyihada bu yerda JWT tekshiriladi.
type Auth struct {
    tokens map[string]Principal
}

func (a *Auth) Require(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
        p, known := a.tokens[token]
        if !ok || !known {
            w.Header().Set("WWW-Authenticate", "Bearer")
            writeError(w, http.StatusUnauthorized, "unauthorized", "tizimga kiring")
            return
        }
        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), principalKey{}, p)))
    })
}

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

func RequireRole(roles ...Role) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            p, ok := PrincipalFrom(r.Context())
            if !ok {
                writeError(w, http.StatusUnauthorized, "unauthorized", "tizimga kiring")
                return
            }
            if !slices.Contains(roles, p.Role) {
                writeError(w, http.StatusForbidden, "forbidden", "bu bo‘lim uchun ruxsat yo‘q")
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

func (a *Auth) protect(perm Permission, h http.HandlerFunc) http.Handler {
    return a.Require(RequirePermission(perm)(h))
}

// ---------- Handlerlar ----------

func ok(msg string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"message": msg})
    }
}

func routes(auth *Auth) http.Handler {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /books", ok("kitoblar ro‘yxati"))
    mux.Handle("POST /books", auth.protect(PermBooksCreate, ok("kitob yaratildi")))
    mux.Handle("DELETE /books/{id}", auth.protect(PermBooksDelete, ok("kitob o‘chirildi")))

    admin := http.NewServeMux()
    admin.HandleFunc("GET /admin/users", ok("foydalanuvchilar ro‘yxati"))
    admin.HandleFunc("GET /admin/stats", ok("statistika"))
    mux.Handle("/admin/", auth.Require(RequireRole(RoleAdmin)(admin)))

    return mux
}

// ---------- Sinov ----------

func main() {
    auth := &Auth{tokens: map[string]Principal{
        "t-reader": {UserID: 1, Role: RoleReader},
        "t-author": {UserID: 2, Role: RoleAuthor},
        "t-admin":  {UserID: 3, Role: RoleAdmin},
    }}
    handler := routes(auth)

    cases := []struct{ method, path, token string }{
        {"GET", "/books", ""},
        {"POST", "/books", ""},
        {"POST", "/books", "t-reader"},
        {"POST", "/books", "t-author"},
        {"DELETE", "/books/5", "t-author"},
        {"DELETE", "/books/5", "t-admin"},
        {"GET", "/admin/stats", "t-author"},
        {"GET", "/admin/stats", "t-admin"},
        {"GET", "/admin/users", "soxta"},
    }

    for _, c := range cases {
        req := httptest.NewRequest(c.method, c.path, nil)
        if c.token != "" {
            req.Header.Set("Authorization", "Bearer "+c.token)
        }
        rec := httptest.NewRecorder()
        handler.ServeHTTP(rec, req)

        fmt.Printf("%-6s %-13s %-9s → %d %s", c.method, c.path, c.token, rec.Code, rec.Body.String())
    }
}
go run main.go
GET    /books                  → 200 {"message":"kitoblar ro‘yxati"}
POST   /books                  → 401 {"error":{"code":"unauthorized","message":"tizimga kiring"}}
POST   /books        t-reader  → 403 {"error":{"code":"forbidden","message":"bu amal uchun ruxsat yo‘q"}}
POST   /books        t-author  → 200 {"message":"kitob yaratildi"}
DELETE /books/5      t-author  → 403 {"error":{"code":"forbidden","message":"bu amal uchun ruxsat yo‘q"}}
DELETE /books/5      t-admin   → 200 {"message":"kitob o‘chirildi"}
GET    /admin/stats  t-author  → 403 {"error":{"code":"forbidden","message":"bu bo‘lim uchun ruxsat yo‘q"}}
GET    /admin/stats  t-admin   → 200 {"message":"statistika"}
GET    /admin/users  soxta     → 401 {"error":{"code":"unauthorized","message":"tizimga kiring"}}

Natijalar kutilganidek:

  • token bo‘lmasa yoki soxta bo‘lsa — 401;
  • token bor, lekin ruxsat yetarli emas — 403;
  • ruxsat bor — handler ishlaydi.

Bu jadval shaklidagi sinov haqiqiy test sifatida ham yozilishi mumkin. Har bir endpoint uchun "kim kira oladi, kim kira olmaydi" ro‘yxatini test ko‘rinishida saqlash avtorizatsiyadagi xatolarni kod production’ga chiqishidan oldin ushlaydi.

Middleware nimani tekshira olmaydi?

Middleware handler ishga tushishidan oldin ishlaydi. U so‘rov haqida faqat yo‘l, metod, sarlavhalar va tokenni biladi. Resursning o‘zi haqida esa hech narsa bilmaydi.

DELETE /books/42
  ✅ middleware biladi: foydalanuvchida books:delete ruxsati bormi?
  ❌ middleware bilmaydi: 42-kitob kimniki?

42-kitobning egasini bilish uchun uni bazadan o‘qish kerak. Bu ish esa handler yoki service qatlamida bajariladi. Shuning uchun ruxsat tekshiruvi ikki qatlamda bo‘ladi:

Qatlam Nimani tekshiradi Qayerda
Endpoint Amal turi: books:delete Middleware
Resurs Egalik: 42-kitob unikimi? Service (oldingi dars)

Ikkinchisi birinchisining o‘rnini bosmaydi va aksincha.

Xulosa

  • Endpoint darajasidagi ruxsat tekshiruvi middleware’ga chiqariladi. Bu takrorlanishni va tekshiruvni unutish xavfini kamaytiradi.
  • Ruxsat middleware’i auth middleware’dan keyin ishlaydi va contextdagi Principaldan foydalanadi.
  • protect kabi yordamchi funksiya route ro‘yxatini o‘qishni osonlashtiradi.
  • Endpointlar guruhini alohida mux’ga yig‘ib, butun guruhni bitta middleware bilan himoyalash mumkin.
  • Middleware resursga egalikni tekshira olmaydi. Bu tekshiruv service qatlamida qoladi.
  • httptest yordamida router’ni haqiqiy serversiz sinash mumkin.

Keyingi darsda 401, 403 va 500 status kodlarini qachon qaytarish kerakligini aniqlab olamiz.