Backendda permission sistema qurish
Kirish
Tasavvur qiling, juma kuni kechqurun product manager sizga yozyapti:
"Nega Buxorodagi manager Toshkentdagi orderlarni ham ko'ryapti?"
"Frontendda refund tugmasi yopiq edi, lekin operator baribir API orqali refund qilib yuboribdi"
"Bitta kompaniya useri boshqa tenantdagi clientlarni search da ko'rib qolgan"
Bu uchala holat odatda bitta sabab orqali kelib chiqadi: authorization modeli platforma kattalashgan sari murakkablashga va juda ham ko'p teshiklar paydo bo'lib qolgan.
Backend yozayotganda authorization avval juda oddiy narsadek ko'rinadi. Odatda boshida bir nechta role bo'ladi: admin, manager, operator, user. Boshida shuni o'zi yetadigandek tuyuladi. Chunki platforma hali kichik bo'ladi, endpointlar kam bo'ladi, talablar ham ko'p emas. Role-based modelni o'zi juda ham yaxshi yechim bo'lib qoladi.
Lekin platforma sal kattalashishi bilan problemalar paydo bo'lishni boshlaydi. Bir manager hamma orderni ko'rmasligi kerak bo'ladi, faqat o'z filialidagini ko'rishi kerak bo'ladi. Operator buyurtmaga izoh yozishi mumkin bo'ladi, lekin refund qila olmaydi. Finance refund qiladi, lekin mahsulotni edit qila olmmaydi. Oddiy user o'z profilini o'zgartiradi, lekin boshqa usernikiga tegmaydi.
Shu joydan boshlab oddiy role modeli yetmay qoladi. Muammo endi "bu user kim?" degan savolda emas, "bu user aynan nima qila oladi, qaysi obyektlar ustida qila oladi va qayergacha qila oladi?" degan savollarda bo'ladi.
Ko'p jamoalarda permission system ustida jiddiy o'ylash faqat birinchi incident'dan keyin boshlanadi yoki umuman bu haqida o'ylab o'tirishmay proyektni hard code qilish if, else qilib tashlashadi. Dastlab bitta endpoint noto'g'ri filter bilan chiqadi, keyin bir role uchun alohida exception qo'shiladi, keyin yana bitta endpoint ichida qo'shimcha if yoziladi. Oradan biroz vaqt o'tib esa proyektni katta qismni qil ustiga qurilgandak bo'lib qoladi. Shu holatda yangi dasturchi qo'shilsa va unga proyektni tushuntirish kerak bo'lsa: "Ishlamayapti, lekin nega? Ishlayapti, lekin nega?"
Ayniqsa CRM, admin panel, ichki boshqaruv tizimlari va multi-tenant ilovalarda permission systemni boshidan to'g'ri o'ylash juda muhim. Aks holda authorization logikasi endpointlarga sochilib ketadi, role'lar soni keragidan ortib ketadi, qoidalar bir-biriga zid bo'lib qoladi va eng yomoni, xavfsizlikdagi xatolar kelib chiqadi.
Bu maqolada backenddagi permission sistemani qanday o'ylash kerakligi, qaysi muammolar tez-tez uchrashi, resource + action + scope modeli nima uchun qulayligi, fastapi da buni qanday arxitektura bilan qurish mumkinligi va taxminiy DB strukturalar qanday bo'lishi haqida gaplashamiz.
Muammo nimada?
Dastlab ko'p loyihalar role-based yondashuv bilan boshlanadi. Masalan:
adminhamma narsani qila oladimanagerma'lum bo'limga javob beradioperatoroddiy operatsiyalarni bajaradiuserfaqat o'ziga tegishli ma'lumotlarni ko'radi
Bu yondashuv kichik tizimlarda ishlaydi. Muammo product kattalashganda boshlanadi. Aniqrog'i, product kattalashgach role modeli hamma savolga bir xil jiddiylik bilan "manager" deb javob berishni bas qilishi kerak bo'ladi.
Chunki real system'da endpointga kirish huquqi bitta savol emas. Odatda kamida uchta savol bo'ladi. Qog'ozda ular sodda ko'rinadi, lekin endpoint ichiga tushganda hammasi ancha jiddiylashadi:
user shu actionni umuman bajara oladimi?
user aynan shu obyekt ustida shu actionni bajara oladimi?
user qaysi scope ichida ishlayapti:
all,branch,own,self?
Masalan, PATCH /orders/845 degan request kelganda "managermi yoki yo'qmi?" degan bitta tekshiruv yetmaydi. Balki quyidagilarni ham bilish kerak bo'ladi:
bu order qaysi tenantga tegishli
order qaysi branch'da yaratilgan
uni kim yaratgan
update qilinayotgan field'lar xavfsizmi
bu action uchun permission bormi yoki faqat ko'rish huquqimi
Role-based model mana shu savollarning barchasiga javob bera olmay qolgan joyda authorization logikasi sinishni boshlaydi. O'sha sinish odatda demo kuni emas, production'da ko'rinadi.
Masalan, CRM tizimida quyidagi holatni olaylik:
call center operatori mijozni ko'ra oladi, lekin shartnomani tasdiqlay olmaydi
filial manageri o'z branch'idagi buyurtmalarni ko'ra oladi, lekin boshqa branch buyurtmalariga kira olmaydi
finance xodimi to'lovni refund qila oladi, lekin mahsulot narxini tahrir qilmaydi
oddiy user o'z profilini tahrir qiladi, lekin boshqa userlarni ko'rmaydi
Mana shu joyda bitta muammo chiqadi: role nomining o'zi yetmay qoladi.
Masalan, manager degan role bor bo'lsa ham, barcha managerlar bir xil emas. Biri faqat o'z filialidagi orderlarni ko'radi, boshqasi bir nechta filial bilan ishlaydi, yana boshqasi ko'rish bilan birga tasdiqlash ham qila oladi. Endi bularning hammasini role nomi bilan ifodalashga urinsak, role'lar ko'payib ketadi.
Yana bir muhim nuqta shuki, action va object doim bir xil emas. User'da orders.update huquqi bo'lishi mumkin, lekin bu barcha orderlarni update qila oladi degani emas. Balki faqat o'zi yaratgan orderlarni yoki faqat o'z branch'idagi orderlarni tahrir qila olar.
Amaliyotda bu ko'pincha juda oddiy ko'rinadigan koddan boshlanadi:
@router.get("/orders")
async def list_orders(user=Depends(get_current_user)):
if user.role not in {"admin", "manager"}:
raise HTTPException(status_code=403, detail="Forbidden")
return await order_service.list_orders()
Birinchi qarashda bu kod ishlayotgandek. Lekin unda muhim savol yo'q: manager qaysi orderlarni ko'rishi mumkin? Natijada manager roli bor har qanday user barcha orderlarni ko'rib ketishi mumkin.
List endpointlar ham alohida bosh og'riq. GET /orders bilan userga qancha ma'lumot chiqishi kerak? U barcha orderni ko'radimi, faqat o'z branch'idagini ko'radimi, yoki faqat o'zi yaratganlarini ko'radimi? Authorization ko'pincha aynan shu joyda parod qilishni boshlaydi.
Agar ilova multi-tenant bo'lsa, Role-based umuman to'g'ri kelmaydi. Boshida oson, ohirida azob bo'lib qoladi ishlash. Bir tenantdagi user boshqa tenantdagi data'ni ko'rmasligi kerak. Bu esa oddiy role tekshiruvdan tashqari alohida isolation qoidalarini ham talab qiladi.
Shu sabab permission system bilan bog'liq eng xavfli buglar ko'pincha "permission yo'q edi-yu, lekin endpoint ishladi" ko'rinishida emas, balki "permission bor edi, lekin scope noto'g'ri talqin qilindi" ko'rinishida chiqadi. Masalan oddiy manager buyurtmalar orasida product name ga qarab search qilganida shu managerning kompaniyasiga tegishli bo'lmagan boshqa kompaniya buyurtmalarini ham olib kelib qo'yishi mumkun.
Shuning uchun permission systemni faqat role bilan hal qilishga urinish odatda ma'lum bosqichdan keyin buziladi.
Eng ko'p uchraydigan noto'g'ri yondashuvlar
Bu xatolar ko'pincha yomon arxitektordan emas, tez o'sayotgan product'dan chiqadi. Dastlab hammasi ishlayotgandek ko'rinadi. Muammo shundaki, bu yondashuvlar odatda aynan production'da, yangi role qo'shilganda yoki birinchi tenantlar ko'payganda sinadi. Authorization ko'p hollarda sokin turgandek ko'rinadi, lekin muammo chiqsa birdan butun sahnani egallaydi.
1. Faqat role bilan yurish
Dastlab quyidagicha model juda qulay ko'rinadi:
adminmanageroperatorfinance
Keyin talablar ko'payadi va sekin-sekin bunday kombinatsiyalar chiqadi:
manager_branch_1manager_branch_2finance_limitedoperator_with_refundreadonly_manager
Bu holat odatda role explosionga olib keladi. Ya'ni asl muammo permission modelida bo'lsa ham, biz uni yangi role qo'shib hard code qilib fix qilishga urunamiz.
Masalan, product tomondan quyidagi talablar keladi:
barcha managerlar orderlarni ko'rsin
faqat ayrim managerlar export qilsin
ayrim filial managerlari approve ham qilsin
ayrim operatorlar refund qilmasin
Agar buni faqat role bilan yechmoqchi bo'lsak, role nomlari business qoida katalogiga aylanib qoladi. Oradan biroz vaqt o'tib esa jamoa "bu userga qaysi role beramiz?" degan savolga javob izlaydi, lekin "bu userga qaysi permissionlar kerak?" degan asosiy savol ortda qoladi. Keyin role nomlari ham pasport seriyasiga o'xshab ketadi: bor, lekin bir qarashda hech narsa anglashilmaydi.
2. Endpoint ichida tarqoq if lar
Ko'p codebaselarda permission tekshiruvlari har endpoint ichida alohida yozilgan bo'ladi:
if user.role == "admin":
...
elif user.role == "manager" and order.branch_id == user.branch_id:
...
Dastlab bunday kod qulay ko'rinadi. Lekin vaqt o'tib bir xil qoida turli endpointlarda turli ko'rinishda yoziladi. Bitta joyda managerga ruxsat beriladi, boshqa joyda unutib yuboriladi, uchinchi joyda esa umuman boshqa semantika ishlatiladi. if user.role == "admin" bilan boshlangan kod ba'zan keyin kichik bir mahalliy qonunchilikka aylanadi.
Masalan, bitta endpointda manager branch bo'yicha tekshiriladi:
if user.role == "manager" and order.branch_id == user.branch_id:
return order
Boshqa endpointda esa xuddi shu manager uchun butunlay boshqa qoida yozilib qoladi:
if user.role == "manager" and order.created_by == user.id:
order.status = payload.status
Endi savol tug'iladi: manager branch bo'yicha ishlaydimi yoki own bo'yicha ishlaydimi? Agar bu semantika markazlashmagan bo'lsa, javob endpointga qarab o'zgarib ketadi.
Xullas bosh og'riq bo'lishni boshlaydi.
3. Frontend tugmani yashirganiga ishonish
Ba'zi joylarda "tugma yo'q, demak user bu actionni qila olmaydi" degan fikr paydo bo'ladi. Bu noto'g'ri. Frontend faqat UX uchun ishlaydi. Asosiy himoya backend'da bo'lishi kerak.
Agar backend aniq permission check qilmasa, user to'g'ridan to'g'ri API chaqirib actionni bajarishi mumkin.
Masalan, frontend refund tugmasini yashirib qo'ydi. Lekin user brauzer console'dan yoki Postman orqali shu request'ni yuborishi mumkin:
POST /payments/914/refund
Authorization: Bearer <token>
Agar backend'da payments.refund uchun check bo'lmasa, tugma yashirilgani hech narsani anglatmaydi. UI'dagi restriction xavfsizlik emas, faqat interfeys qulayligi.
4. DB'dan hamma narsani olib, keyin Python'da filter qilish
List endpointlarda keng tarqalgan xatolardan biri bu barcha ma'lumotni olib kelib, keyin Python tarafida filter qilishdir.
Masalan:
orders = (await session.execute(select(Order))).scalars().all()
orders = [x for x in orders if x.branch_id == user.branch_id]
Bu bir nechta sababga ko'ra yomon:
xavfsizlik zaiflashadi
performance yomonlashadi
pagination noto'g'ri ishlaydi
kelajakda kimdir filter yozishni unutib yuborishi mumkin
To'g'ri yo'l bu filterlarni query darajasida ishlatish.
Ayniqsa bu muammo search, export, count va pagination'da og'riqli bo'ladi. Masalan siz page size 20 bilan millionlab orderdan ma'lumot olayapsiz, lekin avval hammasini olib keyin filter qilyapsiz. Bu nafaqat sekin, balki ba'zi holatda filterdan oldingi data xotiraga tushgani uchun xavfsizlik nuqtai nazaridan ham yomon signal.
5. Barcha permissionlarni JWT ichiga joylash
Boshida "hamma permission token ichida bo'lsa, DB so'ralmaydi" degan fikr zo'rdek ko'rinadi. Lekin keyin muammolar boshlanadi:
token kattalashadi
permission o'zgarganda eski tokenlar muammo bo'ladi
revoke qilish qiyinlashadi
Kimdir aytishi mumkun, JWT da permissionlarni saqlash bu zo'r ideya deb. Qo'shilaman, qiziq ideya. Faqat permission o'zgartirganda JWT ni revoke qila olish kerak. Odatda shu qismi tufayli permissionlarni JWT da ishlatish yomon ideya bo'lib qoladi bazi turdagi platformalar uchun. JWT tokenlarni revoke qilishni o'zini alohida maqolaga olib chiqsak bo'ladi menimcha. Chunki qilish usullari ko'p va hech qaysi birini ideal deb bo'lmaydi.
Ko'p holatda token ichida minimal identity saqlanadi, permissionlar esa DB yoki cache orqali resolve qilinadi. Shunisi eng toza yechim.
Masalan, finance useridan payments.refund huquqi soat 11:00 da olib tashlandi. Lekin uning JWT tokeni kechki 23:00 gacha amal qilsa, backend faqat tokendagi permissionlarga ishonayotgan bo'lsa, user o'sha vaqtgacha refund qilishda davom etishi mumkin.
Yana bir muammo shuki, token payload asta-sekin permission dump'iga aylanadi:
{
"sub": 42,
"tenant_id": 7,
"permissions": [
"orders.read.branch",
"orders.update.branch",
"payments.refund.all",
"reports.export.branch"
]
}
Bu kichik loyihada yomon ko'rinmasligi mumkin. Lekin permissionlar dynamic bo'la boshlaganda, revoke, cache invalidation va session freshness masalalari juda tez chiqadi. Token sekin-sekin access card emas, ko'chma permission arxiviga aylana boshlaydi.
To'g'ri model: resource + action + scope
Permission sistemani boshqariladigan qilish uchun qulay modellardan biri bu resource + action + scope yondashuvidir.
Bu modelning kuchi shundaki, u role nomlarining ichiga yashirinib qolgan semantikani ochiq ko'rinishga olib chiqadi. Ya'ni "manager" yoki "operator" degan umumiy label o'rniga system aniqroq gapira boshlaydi:
qaysi resource ustida
qaysi action bilan
qaysi scope doirasida
Keling, shu tilni bir marta aniq qilib olaylik. Bu modelda permission uch qismga ajraladi.
Resource
Resource bu action qaysi obyekt yoki modul ustida bajarilayotganini bildiradi.
Misollar:
usersorderspaymentsproductsreports
Action
Action bu aynan nima qilinayotganini bildiradi.
Misollar:
readcreateupdatedeleteapproverefundexport
Scope
Scope shu permission qaysi darajagacha ishlashini bildiradi.
Misollar:
allbranchownself
Masalan, bitta CRM tizimida quyidagi permissionlar bo'lishi mumkin:
manager uchun
orders.read+branchmanager uchun
orders.update+branchoperator uchun
clients.read+branchoperator uchun
orders.update+ownfinance uchun
payments.refund+alloddiy user uchun
users.update+self
Asosiy g'oya juda sodda: permission nomi nima qilinayotganini aytadi, scope esa bu ish qayergacha borishini belgilaydi.
Buni kodga yaqinroq ko'rinishda tasavvur qilsak, grant deyarli quyidagicha o'qiladi:
Grant(resource="orders", action="update", scope="branch")
Bu yozuvni o'qigan odam ham, kod ham bitta narsani tushunadi: user orderni update qila oladi, lekin faqat branch scope ichida.
Shu modelning yana bir foydasi shuki, bir xil permission vocabulary turli rolelar uchun qayta ishlatiladi. Masalan:
branch_managerroliorders.read.branchvaorders.update.branchgrantlarini oladifinance_managerrolipayments.read.allvapayments.refund.allgrantlarini oladicustomerroliusers.read.selfvausers.update.selfgrantlarini oladi
Shu nuqtadan boshlab role access ma'nosini o'zi tashimaydi. U shunchaki grantlar to'plamiga aylanadi. Muhim farq ham shu: role label, permission esa mazmun.
Shu model ustiga yana ikki muhim tushuncha qo'shiladi:
role: permissionlar to'plami
policy: murakkab object-level tekshiruvlar uchun alohida qatlam
Authorization qatlamlari
Authorizationni bitta tekshiruv deb o'ylash oson. Amalda esa uni ikki qatlamga ajratib ko'rish ancha toza natija beradi.
Amaliyotda request oqimi ko'pincha quyidagicha bo'ladi:
request keladi va user identity aniqlanadi
user uchun
AuthContextyig'iladicoarse-grained check endpointga umuman kirish mumkinmi, shuni tekshiradi
kerak bo'lsa obyekt DB'dan olinadi
fine-grained policy aynan shu obyekt ustida action mumkinmi, shuni tekshiradi
list endpoint bo'lsa, scope query darajasida qo'llanadi
Shu ajratish foydali, chunki "endpointga kirish mumkinmi?" degan savol bilan "shu konkret orderni edit qilish mumkinmi?" degan savol aslida boshqa-boshqa savollar.
1. Coarse-grained authorization
Bu qatlam user da umuman shu action bor-yo'qligini tekshiradi.
Masalan:
orders.updatepayments.refundusers.read
Agar userda kerakli permission bo'lmasa, shu joyning o'zida deny qilinadi.
fastapi da bu ko'pincha dependency orqali tekshiriladi:
from fastapi import Depends, HTTPException
def get_resolver(auth=Depends(get_auth_context)):
return PermissionResolver(auth)
def require_permission(resource: str, action: str):
def inner(resolver=Depends(get_resolver)):
if not resolver.has_permission(resource, action):
raise HTTPException(status_code=403, detail="Permission denied")
return resolver
return inner
Endpoint misoli:
@router.patch("/orders/{order_id}")
async def update_order(
order_id: int,
payload: OrderUpdateSchema,
resolver=Depends(require_permission("orders", "update")),
):
...
Bu bosqich hali order_id=845 aynan qaysi branchga tegishli ekanini tekshirmaydi. U faqat user da nazariy jihatdan orders.update actioni bor yoki yo'qligini tekshiradi.
2. Fine-grained authorization
Bu qatlam esa aynan shu obyekt ustida action qilish mumkinmi, shuni tekshiradi.
Masalan userda orders.update bor, lekin:
faqat o'z branchidagi orderlarni update qila oladi
yoki faqat o'zi yaratgan orderlarni update qila oladi
yoki faqat o'z tenantidagi data bilan ishlay oladi
Shu nuqtada juda muhim tafovut paydo bo'ladi:
permission user bu actionni umuman sinab ko'rishi mumkinmi, shuni aytadi
policy esa aynan shu record ustida, aynan hozir, aynan shu context'da mumkinmi, shuni aytadi
Bu joyda alohida policy class qulay bo'ladi:
class OrderPolicy:
@staticmethod
def can_update(auth, resolver, order) -> bool:
if auth.is_superadmin:
return True
if auth.tenant_id is not None and order.tenant_id != auth.tenant_id:
return False
scopes = resolver.get_scopes("orders", "update")
if "all" in scopes:
return True
if "branch" in scopes and auth.branch_id is not None and order.branch_id == auth.branch_id:
return True
if "own" in scopes and order.created_by == auth.user_id:
return True
return False
Keyin endpoint ichida:
@router.patch("/orders/{order_id}")
async def update_order(
order_id: int,
payload: OrderUpdateSchema,
resolver=Depends(require_permission("orders", "update")),
session: AsyncSession=Depends(get_session),
):
auth = resolver.auth
order = await session.get(Order, order_id)
if not order:
raise HTTPException(404, "Order not found")
if not OrderPolicy.can_update(auth, resolver, order):
raise HTTPException(403, "Forbidden")
...
Hayotda policy bundan ham boyroq bo'ladi. Masalan manager orderni update qila oladi, lekin:
amountmaydonini emas, faqatstatusni o'zgartiradiapprovedholatdagi orderni qayta edit qila olmaydifaqat ish vaqti ichida ayrim actionlarni qiladi
Shu yerda ko'rinadi: faqat permission string yetmaydi. Obyektga qarab qaror qiladigan policy qatlami ham kerak.
Permission system uchun asosiy prinsiplar
1. Default deny
Agar ruxsat aniq berilmagan bo'lsa, default natija deny bo'lishi kerak. Permission topilmadi degani ruxsat bor degani emas.
Bu prinsip ayniqsa yangi endpoint qo'shilganda asqotadi. Route ochildi, lekin unga grant berilmadi degani endpoint tasodifan hammaga ochilib ketmasligi kerak. Security tizimi odatda "fail closed" ishlagani sog'lomroq. Authorizationda ortiqcha optimizm esa ko'pincha qimmatga tushadi.
2. Least privilege
Foydalanuvchiga faqat ishini bajarish uchun kerak bo'lgan minimal access beriladi. Keragidan ortiq permission vaqt o'tishi bilan xavfga aylanadi.
Amaliyotda eng ko'p xato quyidagicha bo'ladi: "hozircha ishlashi uchun all berib turamiz, keyin toraytiramiz". Odatda o'sha "keyin" kelmaydi. Vaqt o'tib temporary access doimiy access'ga aylanadi. Backend tarixida eng uzoq yashaydigan narsalardan biri vaqtincha berilgan keng permission bo'lsa kerak.
3. Centralized policy
Permission tekshiruvlari kod bo'ylab tarqalib ketmasligi kerak. Ular markazlashgan holda yozilgani yaxshiroq: alohida resolver, dependency yoki policy class orqali.
Yaxshi authorization qatlamining bir belgisi shuki, jamoada kimdir "manager orderni qayerda update qila oladi?" deb so'rasa, javob bitta joydan topiladi. O'nlab endpoint ichidan if user.role == ... qidirib yurilmaydi. Aks holda debugging emas, arxeologiya boshlanadi.
4. Explicit semantics
own, self, branch, all kabi scope'lar aniq ma'noga ega bo'lishi kerak.
Masalan:
self= user o'z recordiown= user yaratgan obyektbranch= user branch'iga tegishli obyektlarall= shu kontekst ichidagi barcha obyektlar
Aks holda bitta codebase ichida bir xil scope turli joyda turli ma'noda ishlay boshlaydi.
Masalan, own ba'zi joyda "o'zi yaratgan", boshqa joyda esa "o'ziga assign qilingan" ma'nosida ishlatilsa, permission modeli endi semantik model bo'lmay qoladi. Scope nomi bilan uning ma'nosi orasida qat'iy bog'lanish bo'lishi kerak.
5. Query-level authorization
List endpointlarda access nazorati SQL query darajasida qo'llanishi kerak. Avval barcha ma'lumotni olib, keyin filter qilish xavfli va sekin.
Masalan, orderlarni list qilishda bunday qilish yaxshiroq:
stmt = select(Order)
stmt = stmt.where(Order.branch_id == auth.branch_id)
rows = (await session.execute(stmt)).scalars().all()
Yoki scope'ga qarab query builder yozish ham mumkin:
def apply_order_scope(stmt, auth):
if auth.is_superadmin:
return stmt
if auth.branch_id is not None:
return stmt.where(Order.branch_id == auth.branch_id)
return stmt.where(False)
Katta loyihalarda buni yana bir pog'ona tartibli qilish foydali: har resource uchun bittadan scope-applier funksiya bo'ladi. Masalan:
apply_order_scope(...)apply_client_scope(...)apply_payment_scope(...)
Shunda list, export, analytics, background job va report endpointlari bir xil authorization semantikasi bilan ishlaydi. Bu nafaqat xavfsizlikni yaxshilaydi, balki kelajakdagi "nega export boshqa natija berdi?" degan savollarni ham kamaytiradi.
6. Tenant boundary first
Agar ilova multi-tenant bo'lsa, avval tenant isolation ishlashi kerak, keyin scope qo'llanadi. Oddiy user hech qachon boshqa tenantdagi data'ni ko'rmasligi kerak.
Amaliy jihatdan bu degani branch, own, all kabi scope'lardan oldin tenant_id filter keladi. Ya'ni all ko'pincha "butun dunyo bo'yicha all" emas, "shu tenant ichida all" degani.
7. Permission rule va business rule bir xil emas
Masalan user'da payments.refund huquqi bo'lishi mumkin. Lekin refund ayni paytda mumkinligi boshqa savol:
payment muvaffaqiyatli bo'lganmi
30 kundan oshmaganmi
limitdan chiqmaganmi
Bular permission emas, business rule.
Bu ikki qatlamni aralashtirib yuborish ko'p chalkashlik beradi. User'da refund permissioni bo'lsa ham, response 403 emas, ayrim holatda 409 yoki 400 bo'lishi kerak bo'lgan case'lar chiqadi. Chunki muammo "kim qilishga haqli?" emas, "shu action hozir business state bo'yicha mumkinmi?" degan savolda bo'ladi.
8. Backend always checks
Frontend hech qachon asosiy himoya qatlami bo'la olmaydi. Permissionning yakuniy tekshiruvi backend'da bo'lishi kerak.
Frontend permission vocabulary'dan foydalansa bo'ladi: masalan tugmani ko'rsatish yoki yashirish uchun. Lekin bu faqat UX optimizatsiya. Asosiy qaror backend'da qabul qilinadi.
Amaliy muammolar va ularning yechimlari
Role explosion
Agar system faqat role bilan yuradigan bo'lsa, vaqt o'tishi bilan role'lar ko'payib ketadi. Buni yechish uchun role faqat grouping vazifasini bajarsin, haqiqiy access esa permission + scope orqali ifodalansin.
Amaliy yechim odatda ikki bosqichli bo'ladi:
permission vocabulary'ni ajratib olish:
orders.read,orders.update,payments.refundrole'larni shu permissionlar to'plamiga aylantirish
Shunda yangi talab kelganda har safar yangi role yasash shart bo'lmaydi. Ko'pincha mavjud role'ga bitta grant qo'shish yoki scope'ni o'zgartirish yetadi.
Object-level access
User'da action bo'lishi hali hamma object uchun ruxsat bor degani emas. Buni alohida policy layer orqali tekshirish qulay.
Masalan, sales operatori clients.read huquqiga ega bo'lishi mumkin, lekin faqat o'z regionidagi clientlarni ko'rishi kerak. Shunday bo'lsa, faqat endpointga kirish mumkinligini tekshirish yetmaydi.
Yechim shuki, detail endpointlar uchun object-level policy majburiy qatlamga aylantiriladi. Ya'ni:
coarse check endpointga kirishni tekshiradi
policy esa konkret record ustidagi access'ni tekshiradi
Bu ajratish ko'plab yashirin bug'larni erta ushlaydi.
List endpoint xavfsizligi
List endpointlar detail endpointlardan ko'ra ko'proq data oqimini boshqaradi. Shu sabab query-level filtering muhim. Bu ayniqsa admin panel va CRM'larda seziladi.
Masalan, GET /clients noto'g'ri yozilgan bo'lsa, bittagina xato butun bazani ko'rsatib yuborishi mumkin.
Yechim sifatida list endpointlarni alohida xavf kategoriyasi deb qarash foydali. Chunki bir dona detail endpoint xatosi bitta recordni ochishi mumkin, list endpoint xatosi esa minglab recordni chiqarib yuboradi. Shu sabab:
filter query'da bo'lishi kerak
pagination filterdan keyin ishlashi kerak
count,export,searchendpointlari ham xuddi shu scope logic'dan foydalanishi kerakscope logic bittadan ko'p joyda copy-paste qilinmasligi kerak
Multi-tenant leakage
Agar tenant boundary aniq qo'llanmasa, boshqa tenantdagi recordlar ko'rinib qolishi mumkin. Bu permission system'dagi eng xavfli xatolardan biri.
Masalan, bir kompaniyaning operatori boshqa kompaniya mijozlarini ko'rib qolsa, bu endi shunchaki bug emas, to'g'ridan to'g'ri xavfsizlik muammosi.
Amaliy yechim shuki, tenant filter optional emas, bazaviy qatlam bo'ladi. Ko'p jamoalarda buni repository yoki base query helper ichiga joylash foydali:
def apply_tenant_scope(stmt, auth):
if auth.is_superadmin:
return stmt
return stmt.where(Order.tenant_id == auth.tenant_id)
Keyin shuning ustiga branch yoki own filtrlari qo'llanadi.
Deny va allow precedence
Agar system'da allow va deny ikkalasi ham bo'lsa, qaysi biri ustun ekanini oldindan belgilash kerak. Odatda qulay model quyidagicha bo'ladi:
superadmin
explicit deny
explicit allow
default deny
Bu qoidani boshidan belgilab qo'ymaslik keyin juda qimmatga tushadi. Chunki bir user'da role orqali allow, user-level override orqali deny bo'lishi mumkin. Agar precedence aniq bo'lmasa, "nega bu endpoint bir joyda ishlayapti, boshqa joyda yo'q?" degan sirli bug'lar chiqadi. O'sha paytda debugger emas, detektiv kerak bo'lib qoladi.
Cache eskirishi
Permissionlar cache'da saqlansa, ularning eskirib qolish muammosi chiqadi. Masalan managerdan refund huquqi olib tashlandi, lekin Redis ichida eski grantlar turibdi. Bu muammoni versioning yoki aniq invalidation strategiyasi bilan boshqarish mumkin.
Bu yerda odatda uchta yondashuv ishlatiladi:
qisqa TTL
permission version bilan cache invalidation
permission o'zgarganda event chiqarish
Eng muhimi, permission cache "bor ekan, qolaversin" tipidagi yordamchi optimizatsiya bo'lishi kerak. U authorizationning yagona truth source'iga aylanib qolmasligi kerak.
FastAPI'da implementatsiya skeleti
FastAPI'da permission sistemani bir necha alohida qatlamga bo'lib qurish qulay.
Bu yerda maqsad tayyor framework sotish emas. Maqsad authorization logikasi qayerda yashashi kerakligini joy-joyiga qo'yib olish. Ya'ni qo'rqmang, hozir kichik bir auth platforma vendoriga aylanib ketmaymiz:
identity qayerda olinadi
grantlar qayerda resolve qilinadi
coarse check qayerda ishlaydi
policy qayerda ishlaydi
query filter qayerda qo'llanadi
AuthContext
Har request uchun authorization kontekst yig'iladi. Masalan:
user_idtenant_idbranch_idis_superadmingrants
Buni dataclass orqali ifodalash mumkin:
from dataclasses import dataclass
from typing import Any
@dataclass
class Grant:
resource: str
action: str
scope: str
effect: str = "allow"
conditions: dict[str, Any] | None = None
@dataclass
class AuthContext:
user_id: int
tenant_id: int | None
branch_id: int | None
is_superadmin: bool
grants: list[Grant]
Bu kontekst keyingi barcha tekshiruvlar uchun asos bo'ladi.
Amaliyotda AuthContext ko'pincha JWT ichidagi minimal identity va DB'dan olingan grantlar asosida yig'iladi:
async def get_auth_context(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_session),
) -> AuthContext:
claims = decode_access_token(token)
user = await session.get(User, int(claims["sub"]))
grants = await permission_repo.load_user_grants(session, user.id)
return AuthContext(
user_id=user.id,
tenant_id=user.tenant_id,
branch_id=user.branch_id,
is_superadmin=user.is_superadmin,
grants=grants,
)
Muhim nuqta shuki, bu qatlam hali "refund mumkinmi?" yoki "shu orderni edit qila oladimi?" degan qaror chiqarmaydi. U faqat authorization uchun kerak bo'ladigan context'ni yig'adi. Qisqasi, AuthContext hali hech kimni jazolamaydi, vaziyatni tushunib oladi xolos.
PermissionResolver
Bu qatlam user'dagi grantlarni tahlil qiladi va quyidagi savollarga javob beradi:
user'da umuman shu permission bormi
shu resource va action uchun qaysi scope'lar bor
Masalan:
class PermissionResolver:
def __init__(self, auth: AuthContext):
self.auth = auth
def _matching_grants(self, resource: str, action: str) -> list[Grant]:
return [
g
for g in self.auth.grants
if g.resource == resource and g.action == action
]
def has_permission(self, resource: str, action: str) -> bool:
if self.auth.is_superadmin:
return True
grants = self._matching_grants(resource, action)
if any(g.effect == "deny" for g in grants):
return False
return any(g.effect == "allow" for g in grants)
def get_scopes(self, resource: str, action: str) -> set[str]:
if self.auth.is_superadmin:
return {"all"}
grants = self._matching_grants(resource, action)
denied_scopes = {g.scope for g in grants if g.effect == "deny"}
return {
g.scope
for g in grants
if g.effect == "allow" and g.scope not in denied_scopes
}
Bu qatlamning foydasi shundaki, endpointlar grantlarni qo'lda aylanib chiqmaydi. Ular resolver.has_permission(...) yoki resolver.get_scopes(...) orqali bir xil semantikadan foydalanadi. Grantlarni har endpoint ichida qo'lda kavlash esa kamdan-kam hollarda yaxshi yakun topadi.
Dependency orqali coarse check
FastAPI'da Depends(...) orqali coarse-grained authorization qilish qulay. Endpoint boshida kerakli action bor yoki yo'q tekshiriladi.
Masalan:
def get_resolver(auth: AuthContext = Depends(get_auth_context)) -> PermissionResolver:
return PermissionResolver(auth)
def require_permission(resource: str, action: str):
def inner(resolver: PermissionResolver = Depends(get_resolver)) -> PermissionResolver:
if not resolver.has_permission(resource, action):
raise HTTPException(status_code=403, detail="Permission denied")
return resolver
return inner
Bu yondashuv qulay, chunki endpoint ichiga allaqachon tekshirilgan resolver kiradi. Endpoint ichida auth ni alohida yig'ib, yana grant tekshirishga hojat qolmaydi.
Policy orqali fine-grained check
Object-level tekshiruvlar alohida policy class'larda yozilgani yaxshi. Masalan:
OrderPolicy.can_update(...)UserPolicy.can_view(...)ClientPolicy.can_read(...)
Yaxshi policy qatlami faqat True/False qaytarmaydi, balki kerak bo'lsa field-level yoki state-level qarorlarni ham boshqaradi. Masalan:
OrderPolicy.can_update_status(...)OrderPolicy.can_edit_amount(...)PaymentPolicy.can_refund(...)
Shunda permission modeli va business state logikasi bir-biriga chalkashib ketmaydi.
Query filtering
List endpointlarda esa scope asosida SQL query'ning o'ziga filter qo'llanadi. Bu usul xavfsizroq va samaraliroq.
Masalan:
from sqlalchemy import false, select
def apply_tenant_scope(stmt, auth: AuthContext):
if auth.is_superadmin:
return stmt
return stmt.where(Order.tenant_id == auth.tenant_id)
def apply_order_read_scope(stmt, resolver: PermissionResolver):
auth = resolver.auth
if auth.is_superadmin:
return stmt
scopes = resolver.get_scopes("orders", "read")
if "all" in scopes:
return stmt
if "branch" in scopes and auth.branch_id is not None:
return stmt.where(Order.branch_id == auth.branch_id)
if "own" in scopes:
return stmt.where(Order.created_by == auth.user_id)
return stmt.where(false())
Keyin endpoint:
@router.get("/orders")
async def list_orders(
resolver: PermissionResolver = Depends(require_permission("orders", "read")),
session: AsyncSession=Depends(get_session),
):
stmt = select(Order)
stmt = apply_tenant_scope(stmt, resolver.auth)
stmt = apply_order_read_scope(stmt, resolver)
return (await session.execute(stmt)).scalars().all()
Shu usulning eng katta afzalligi shundaki, bitta scope semantikasi list, export, count va background processing oqimlarida bir xil ishlatiladi. Permission mapping DB'da dynamic bo'lishi mumkin, lekin permissionning semantikasi kodda boshqariladi. Natijada ham kod, ham querylar bir-biriga kamroq xafa bo'ladi.
Taxminiy DB struktura
Permission sistemani oddiy va boshqariladigan qilish uchun quyidagi jadvallar yetarli bo'lishi mumkin.
Bu minimal, lekin foydali skelet. Ko'p jamoalar boshida bundan murakkabroq model chizmoqchi bo'ladi. Amalda esa avval permission katalogi va grant bog'lanishlari toza bo'lgani muhimroq. Har doim birinchi kundan boshlab kichik kosmik stansiya darajasidagi RBAC platforma qurish shart emas.
1. permissions
Bu jadval permission katalogi bo'lib xizmat qiladi.
Asosiy ustunlar:
idresourceactioncode
Masalan:
orders.readorders.updatepayments.refund
Taxminiy ko'rinish:
create table permissions (
id bigserial primary key,
resource varchar(64) not null,
action varchar(64) not null,
code varchar(128) generated always as (resource || '.' || action) stored,
unique (resource, action),
unique (code)
);
Bu jadval odatda application vocabulary hisoblanadi. Ya'ni product ichida mavjud actionlar shu yerda katalog sifatida turadi. Ko'p hollarda bu jadval migratsiya yoki seed orqali boshqariladi, admin panel orqali erkin o'zgartirilmaydi.
2. roles
Role'lar shu jadvalda saqlanadi.
Asosiy ustunlar:
idnamecodetenant_idnullableis_system
Agar tenant_id bo'lsa, role tenant-specific bo'lishi mumkin.
create table roles (
id bigserial primary key,
name varchar(64) not null,
code varchar(64) not null,
tenant_id bigint null,
is_system boolean not null default false,
unique (tenant_id, code)
);
is_system foydali ustun, chunki ayrim role'lar productning o'zi bilan keladi, ayrimlari esa tenant admin tomonidan yaratiladi. Shunda system role bilan custom role'ni ajratish osonlashadi.
3. user_roles
User va role orasidagi bog'lanish shu jadvalda saqlanadi.
Asosiy ustunlar:
user_idrole_id
create table user_roles (
id bigserial primary key,
user_id bigint not null references users(id) on delete cascade,
role_id bigint not null references roles(id) on delete cascade,
unique (user_id, role_id)
);
Ko'p hollarda user'da bir nechta role bo'lishi mumkin. Masalan, kimdir ham branch manager, ham finance reviewer bo'lishi mumkin. Shu sabab "bitta user = bitta role" modeli amaliyotda tez torlik qiladi.
4. role_permissions
Role'ga qaysi permission berilganini shu jadval ifodalaydi.
Asosiy ustunlar:
role_idpermission_idscopeeffectconditions_jsonoptional
Bu yerda muhim nuqta shuki, scope permissionning o'zida emas, aynan grant'da saqlanadi. Chunki bitta permission turli role'larda turli scope bilan berilishi mumkin.
create table role_permissions (
id bigserial primary key,
role_id bigint not null references roles(id) on delete cascade,
permission_id bigint not null references permissions(id) on delete cascade,
scope varchar(32) not null,
effect varchar(16) not null default 'allow',
conditions_json jsonb null,
unique (role_id, permission_id, scope, effect)
);
Bu jadval permission system'ning yuragi hisoblanadi. Eng muhim dizayn qarori shuki, scope permission katalogida emas, aynan grantning o'zida turadi. Shu sabab:
orders.readbir role uchunbranchbo'lishi mumkinayni shu
orders.readboshqa role uchunallbo'lishi mumkin
conditions_json esa ehtiyotkorlik bilan ishlatiladi. U yengil dinamik shartlar uchun foydali bo'lishi mumkin, lekin murakkab business logic'ni JSON ichiga ko'mib yuborish yaxshi emas.
5. user_permissions
Bu jadval optional bo'lib, user-level override uchun ishlatiladi.
Masalan:
bir user'dan qo'shimcha permission olib qo'yish
yoki vaqtincha maxsus ruxsat berish
create table user_permissions (
id bigserial primary key,
user_id bigint not null references users(id) on delete cascade,
permission_id bigint not null references permissions(id) on delete cascade,
scope varchar(32) not null,
effect varchar(16) not null default 'allow',
conditions_json jsonb null,
unique (user_id, permission_id, scope, effect)
);
Bu jadvalni ham ortiqcha ishlatish kerak emas. Agar har ikkinchi foydalanuvchiga alohida override berila boshlasa, demak role modeli yoki permission guruhlari uncha yaxshi o'ylanmagan bo'lishi mumkin. System bu bilan sizga nimanidir aytmoqchi bo'ladi.
Bu struktura ko'p hollarda yetarli bo'ladi. Keyin kerak bo'lsa quyidagi qo'shimcha qatlamlar qo'shiladi:
audit_logsyokipermission_audit_logspermission cache
permission versioning
admin UI uchun change history
Multi-tenant bo'lsa nima o'zgaradi?
Multi-tenant ilovada permission sistemaning o'zi yetmaydi. Tenant isolation ham bazaviy qoida sifatida ishlashi kerak.
Bu holatda odatda quyidagilar paydo bo'ladi:
user'da
tenant_idbo'ladiresource'larda
tenant_idbo'ladirole global yoki tenant-specific bo'lishi mumkin
oddiy user uchun
allko'pincha "shu tenant ichida all" degani bo'ladi
Amaliyotda ko'pincha quyidagi model qulay:
global access faqat
superadminyoki alohida global role orqali beriladioddiy userlar esa har doim current tenant ichida ishlaydi
branch,own,selfkabi scope'lar tenant ichida qo'llanadi
Masalan, SaaS CRM tizimida Toshkentdagi bir kompaniya operatori Samarqanddagi boshqa kompaniya mijozlarini ko'rmasligi kerak. Bu endi shunchaki permission masalasi emas, systemdagi asosiy isolation qoidasi.
Amalda tartib odatda bitta: avval tenant filter, keyin scope filtrlari.
Amaliy oqim odatda shunday bo'ladi:
request current tenant context bilan keladi
user shu tenant ichida ishlashga haqli ekanligi tekshiriladi
query'ga tenant filter qo'llanadi
undan keyin
branch,own,selfscope'lari ishlatiladi
Masalan:
stmt = select(Order)
if not auth.is_superadmin:
stmt = stmt.where(Order.tenant_id == auth.tenant_id)
stmt = apply_order_read_scope(stmt, resolver)
Bu yerda juda muhim semantik nuqta bor: oddiy user uchun all ko'pincha "global all" emas. U "current tenant ichidagi all" degani.
Yana bir nozik holat support yoki internal admin userlar bilan chiqadi. Agar ular bir nechta tenant bilan ishlasa, bu access alohida global context bilan boshqarilgani yaxshiroq. Oddiy tenant user logikasi bilan support access'ni aralashtirib yuborish keyinchalik xavfli bo'ladi.
Nimalarni dynamic qilish kerak, nimalarni kodda qoldirish kerak?
Permission sistemada hamma narsani dynamic qilish kerak emas.
Dynamic bo'lishi mumkin bo'lgan qismlar
role yaratish
role'ga permission biriktirish
user'ga role berish
scope tanlash
user override berish
Kodda qolishi kerak bo'lgan qismlar
permission vocabulary
scope semantikasi
object-level policy logic
tenant boundary semantics
sensitive business rules
Masalan, payments.refund permissionini role'ga berish dynamic bo'lishi mumkin. Lekin refund qachon mumkinligi, qaysi orderlar ustida mumkinligi yoki multi-tenant boundary qanday ishlashi kodda boshqarilgani yaxshiroq.
Yana bitta amaliy qoida foydali: admin panel mapping'ni boshqarsin, semantikani emas. Ya'ni admin panel:
orders.read.branchni kimga berishni boshqarishi mumkinlekin
branchdegani nima ekanini o'zgartirmasligi kerakyoki
refundactionining biznes qoidalarini yozmasligi kerak
Aks holda authorization modeli vaqt o'tib "low-code qoidalar to'plami"ga aylanadi va undan keyin nima qaerda ishlashini tushunish qiyinlashadi. Admin panel sekin-sekin kichik bir programming language bo'lib qoladi, bu esa odatda yaxshi yangilik emas.
Masalan, district degan yangi scope paydo bo'lsa, uni shunchaki DB'ga yozib qo'yish yetmaydi. Uning semantikasi, query filteri va policy qoidalari ham kod darajasida qo'shilishi kerak. Aks holda system'da nom bor, lekin ma'no yo'q bo'ladi.
Eng sog'lom formula quyidagicha:
dynamic mapping + stable semantics
Ya'ni kimga nima berilishi DB'da boshqariladi, lekin permissionning ma'nosi va qanday tekshirilishi kodda qat'iy turadi.
Xulosa
Permission system backenddagi eng muhim arxitektura qatlamlaridan biri. U faqat "kim admin" degan savolga javob berish uchun kerak emas. To'g'ri qurilgan authorization modeli proyekt kattalashganda ham kod bazani boshqariladigan holatda ushlab turadi.
Agar tizim kichik bo'lsa, oddiy role modeli bir muddat yetishi mumkin. Lekin real loyihalarda tez orada action, object, scope, tenant va business rule'lar orasidagi farq paydo bo'ladi. Shu nuqtadan boshlab strukturali yondashuv kerak bo'ladi.
resource + action + scope modeli ko'p holatlarda yaxshi boshlanish nuqtasi hisoblanadi. FastAPI'da esa bu modelni AuthContext, PermissionResolver, policy layer va query-level filtering yordamida amaliy va toza ko'rinishda qurish mumkin.
Yaxshi permission system'ning foydasi faqat xavfsizlikda emas. U product jamoasining savollariga ham tez javob beradi:
"kim nimani qila oladi?"
"nima uchun bu user shu recordni ko'rmayapti?"
"yangi role qo'shsak, qayerlarni o'zgartiramiz?"
"multi-tenant leakage'ni qayerda to'sayapmiz?"
To'g'ri permission system productni faqat himoya qilmaydi, backend kod bazasini ham tartibli, tushunarli va kengaytirishga yaroqli holatda ushlab turadi. Eng muhimi, juma oqshomidagi "nega bu user buni ko'rib turibdi?" degan savollar soni ancha kamayadi.
