Autentifikatsiya va xavfsizlik
Argon2id, sessiya va JWT, o‘sib boruvchi kechikish bilan bloklash, har bir formada CSRF va default-src 'none' bilan boshlanadigan CSP.
Mavjud v0.1.0
Foydalanuvchi modeli
Har qanday model. Meros olinadigan asosiy klass yo‘q — aynan shuning uchun mavjud sxema hech qanday migratsiyasiz admin panelga ega bo‘lishi mumkin.
fort.set_user_model(User, identity_field="email")
Beshta atribut odatiy nomlar asosida aniqlanadi — identifikator (identity), parol, faollik (active), xodim (staff), superfoydalanuvchi (superuser). Agar ular boshqacha nomlangan bo‘lsa, ularni aniq ko‘rsating:
fort.set_user_model(
User,
identity_field="login",
password_field="pwd",
active_field="enabled",
staff_field="can_admin",
superuser_field="is_root",
)
Atribut nomlarini aniqlash backend orqali amalga oshiriladi, klass orqali
emas. SQLAlchemy har bir ustun uchun klassda deskriptor yaratadi, Tortoise esa
yo‘q — uning maydonlari faqat Model._meta ichida yashaydi — shu sababli
email maydoni aniq mavjud bo‘lgan Tortoise modeli uchun ham
hasattr(User, "email") False qaytaradi.
Parollar
Argon2id, pwdlib orqali. Parol ustuni turi yoki nomi bo‘yicha aniqlanadi,
shuning uchun hashed_password deb nomlangan oddiy String hech qanday
qo‘shimcha ko‘rsatmasiz to‘g‘ri boshqaruv elementini oladi: yangi parol
maydoni va uni tasdiqlash, saqlashda heshlanadi va ikkalasi ham bo‘sh
bo‘lsa, o‘zgarishsiz qoladi.
FastFortSettings(
auth={"password_min_length": 10, "password_reject_common": True},
)
Maxfiy maydonlar hech qachon forma ichiga qaytarib ko‘rsatilmaydi va audit yozuvlarida niqoblanadi.
Bloklash
auth={
"lockout_threshold": 5,
"lockout_seconds": 60,
"lockout_window_seconds": 900,
}
O‘n besh daqiqa ichida beshta muvaffaqiyatsiz urinish identifikatorni va manzilni bir daqiqaga bloklaydi, va har bir keyingi muvaffaqiyatsiz urinish bilan kechikish ortib boradi.
Sessiyalar va tokenlar
Ikkita yashash muddati — chunki bu ikki xil narsa:
auth={
"session_ttl": 1_209_600, # an admin browser session
"access_token_ttl": 900, # a JWT for an API client
"refresh_token_ttl": 1_209_600,
}
Insonning ish kuni — API mijozining yangilash oynasi bilan bir xil emas.
Admin panelning o‘zi faqat sessiya cookie’sidan foydalanadi va boshqa hech
nimadan, va bu ataylab shunday qilingan. localStoragedagi token sahifaga
kirib qolgan har qanday skript tomonidan o‘qilishi mumkin; cookie ichidagi
token esa, aslida, qo‘shimcha bosqichlari bo‘lgan sessiya cookie’sidir. JWT
loyihaning admin paneli yonida quriladigan API uchun mo‘ljallangan — mobil
mijoz, integratsiya, xizmat hisobi — ya’ni cookie ombori va CSRF masalasi
bo‘lmagan joylar uchun.
Token API
So‘ralganda auth_url manzilida ulanadigan to‘rtta marshrut:
FastFortSettings(
secret_key=...,
auth={"api_enabled": True},
)
POST /auth/token identity + password → access + refresh
POST /auth/refresh refresh → access + refresh
POST /auth/logout refresh → 204
GET /auth/me Bearer access → who that is
Sukut bo‘yicha o‘chirilgan. FastFort qo‘shadigan boshqa har qanday marshrut admin panelning darvozasi orqasida joylashgan; bu to‘rttasi esa zaruriyat tufayli ochiq, va boshqa birovning ilovasiga so‘ralmagan holda ochiq endpoint qo‘shish — kutubxonaning ishi emas.
So‘rov va javob shakllari OAuth 2 standartiga mos, shuning uchun har qanday
avtomatik yaratilgan mijoz va boshqa istalgan API’dan olingan har qanday
curl misoli hech qanday o‘zgarishsiz ishlaydi:
curl -X POST http://127.0.0.1:8000/auth/token \
-d identity=admin@example.com -d password=...
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 900
}
POST /auth/token aynan admin panelning o‘zining kirish formasi qiladigan
chaqiruv orqali autentifikatsiya qiladi, shuning uchun bloklash
hisoblagichlari, vaqt bo‘yicha xavfsiz taqqoslash va kirishda qayta heshlash
— barchasi qo‘llaniladi, faqat bitta farq bilan: u xodim (staff) hisobini
talab qilmaydi. API foydalanuvchilari — bu mijozlar, va kimnidir faqat
administrator emasligi uchun autentifikatsiya qilmaslik — odatiy holatni rad
etish bo‘lardi. Faqat admin panel bilan bir xil odamlarga xizmat ko‘rsatadigan
API uchun auth={"api_requires_staff": True} sozlang. is_active har
ikkala holatda ham talab qilinadi.
O‘z marshrutlaringizni himoyalash
Yuqoridagi to‘rtta marshrut kerak bo‘lmasa ham, ega bo‘lishga arziydigan qism:
from typing import Annotated, Any
from fastapi import Depends
from fastfort.auth import bearer_user
current_user = bearer_user(fort)
@app.get("/orders")
async def orders(user: Annotated[Any, Depends(current_user)]) -> list[Order]:
return await orders_for(user)
Marshrutga claims lug‘ati emas, balki foydalanuvchingizning o‘z qatori (row) beriladi — o‘sha qatorni o‘zi qidirib topishi kerak bo‘lgan marshrutga javobning faqat yarmi berilgan bo‘ladi. Yo‘l davomida imzo tekshiriladi, tokenning yangilash emas, balki aynan kirish (access) tokeni ekanligi tekshiriladi va shu vaqt ichida faolsizlantirilgan foydalanuvchi rad etiladi. Ushbu oxirgi tekshiruv — o‘n besh daqiqada muddati tugaydigan token bilan o‘n besh daqiqa davomida ishlashda davom etadigan yopilgan hisob o‘rtasidagi farqdir.
Shaxsiy kirish tokenlari
Yuqoridagi JWT oqimi ortida odam turgan mijozga to‘g‘ri keladi. U cron vazifasi, joylashtirish skripti yoki hamkor integratsiyasi uchun noto‘g‘ri shakl: ularda kiritiladigan parol yo‘q, tunning uchida yangilash imkoni yo‘q va muvaffaqiyatsiz yangilashni sezadigan hech kim yo‘q. Ularga bitta uzoq muddatli, alohida bekor qilinadigan satr kerak.
Jadval sizniki — jadval talab qiladigan har bir FastFort imkoniyati shunday ishlaydi:
from fastfort.orm.sqlalchemy import ApiTokenMixin
class ApiToken(ApiTokenMixin, Base):
__tablename__ = "admin_api_token"
fort.enable_api_tokens(ApiToken)
Ustunlar shu yerning o‘zida tekshiriladi, shuning uchun sxemadagi xato ishga tushirish bosqichida chiqadi, birinchi autentifikatsiyalangan so‘rovdagi 500 sifatida emas.
Qanday yasaladi
Adminadan, modelni ro‘yxatga olib:
from fastfort.admin import ApiTokenAdmin
fort.register(ApiToken, ApiTokenAdmin)
Qo‘shish tugmasi token yasash sahifasiga aylanadi — sirni terish emas, generatsiya qilish kerak. Sahifa tokenni roppa-rosa bir marta ko‘rsatadi va uni qayta ko‘rsata olmaydi: u qayta yo‘naltirmaydi, balki chizadi, shuning uchun ko‘rsatishning qaytib boriladigan URL’i yo‘q, sahifani yangilash esa yasash formasini qaytaradi.
Yoki kod bilan — boshlang‘ich to‘ldirish skriptiga aynan shu kerak:
issued = await fort.api_tokens.issue(
user=user,
name="nightly-export",
scopes="orders:read", # ta’rifi sizniki; FastFort baho bermaydi
expires_at=None, # None — token muddatsiz
)
print(issued.secret) # u o‘qilishi mumkin bo‘lgan yagona payt
Qanday ishlatiladi
from fastfort.auth import token_user
machine = token_user(fort)
@app.get("/orders")
async def orders(user: Annotated[Any, Depends(machine)]) -> list[Order]:
return await orders_for(user)
bearer_userdan alohida dependency, ikkalasini ham qabul qiladigan bittasi
emas. JWT — ortida odam turgan sessiya, token esa mashinaga berilgan doimiy
ruxsat; ikkalasini ham qabul qiladigan marshrut buni ikkalasiga bog‘lanib
aytishi kerak, jimgina kengaygan yordamchidan meros qilib olib emas.
Nima saqlanadi
Sir emas. Qatorda SHA-256 dayjesti va sakkiz belgili prefiks qoladi, prefiks esa aynan shuning uchun bor: adminadagi qatorni konfiguratsiya faylingizdagi satr bilan solishtira olasiz, lekin qatorning o‘zi autentifikatsiya uchun yetarli bo‘lmaydi.
Dayjest Argon2 emas, SHA-256, va bu ataylab. Murakkablik omili taxmin
qilishni qimmatga aylantirish uchun bor, taxmin esa faqat taxmin qilinadigan
sirga tahdid — kichik va qiyshiq taqsimotdan olingan inson paroliga. Bu yerda
esa secrets.token_urlsafedan 32 bayt: 256 bit bir tekis entropiya va hech
qanday lug‘at yo‘q. Murakkablik omiliga sekinlashtiradigan narsa yo‘q, uni shu
yerga qo‘yish esa har bir autentifikatsiyalangan so‘rovga Argon2 sozlangan
o‘sha ~100 ms’ni yuklardi.
Rad etishlar
Noma’lum, bekor qilingan, muddati o‘tgan yoki o‘chirilgan va faolsizlantirilgan hisobga tegishli — to‘rttasi ham bir xil javob beradi. Ularni farqlash satrni ushlab turgan kishiga to‘rttadan qaysi biri ekanini aytib qo‘yardi.
Kirish yozuvidan farqli o‘laroq, token o‘z hisobidan uzoq yashamasligi kerak, shuning uchun egasi har bir so‘rovda qaytadan qidiriladi. Bekor qilish qatorni saqlaydi, chunki qator hamon bu nima bo‘lgani va qachon to‘xtaganini aytadi; o‘chirish esa bu umuman mavjud bo‘lmasligi kerak edi, deydi.
Admina qiladigan ishga munosabat bildirish
fort.hooks — hech narsani meros qilib olmasdan yozuvga munosabat bildirish
usuli:
from fastfort.core.hooks import Hook
@fort.hooks.on(Hook.AFTER_CREATE)
async def announce(*, request, model_key, obj, **_):
await queue.publish("created", model_key, obj.id)
Hodisani admina egalik qiladigan har bir yozuv yo‘li chiqaradi: forma orqali yaratish va o‘zgartirish, yakka va ommaviy o‘chirish, tiklash, kataklarni joyida tahrirlash, ommaviy tahrirlash va import.
Qachon ishga tushishi — tinglovchi aynan shunga qarab yoziladi.
BEFORE_* tranzaksiya ichida, yozuvdan bevosita oldin bajariladi, shuning
uchun istisno tashlagan tinglovchi uni to‘xtatadi — veto imkonini beradigan
narsa aynan shu:
@fort.hooks.on(Hook.BEFORE_DELETE)
async def protect(*, obj, **_):
if obj.is_locked:
raise ValidationError("Locked records cannot be deleted.")
AFTER_* tranzaksiya tasdiqlangandan keyin bajariladi, shuning uchun
AFTER_CREATE qator shunchaki flush qilingan emas, balki bardoshli ekanini
bildiradi. Xat yuboradigan yoki navbatga hodisa qo‘yadigan tinglovchi keyin
orqaga qaytarilgan o‘zgarish uchun hech qachon ishga tushmaydi — lekin ayni shu
narsa AFTER_*dagi istisno endi yozuvni bekor qila olmasligini va bazani
o‘zgartirgan so‘rovda xato sifatida chiqishini ham bildiradi. Tinglovchi
xatolari ataylab tarqaladi: jimgina yiqiladigan hook hookning yo‘qligidan
yomonroq, chunki chaqiruvchi ish bajarildi deb hisoblaydi.
Ommaviy yo‘l bitta tranzaksiyada ko‘p qator yozadi, shuning uchun partiyada har
bir BEFORE_* har bir AFTER_*dan oldin keladi, ular oralashib ketmaydi. Aks
holda bitta qator bardoshli deb e’lon qilinardi, o‘sha tranzaksiyadagi keyingi
qator esa uni hamon qaytarib olishi mumkin bo‘lardi.
Bilib qo‘yishga arziydigan ikki tafsilot: yumshoq o‘chirish delete juftligini
chiqaradi, update emas — mexanikasi bo‘yicha bitta ustunga yozuv, niyati
bo‘yicha o‘chirish. Va BEFORE_CREATE instansiya emas, tozalangan qiymatlarni
olib yuradi, chunki instansiya hali yo‘q.
Rotatsiya va qayta ishlatish (replay) nimani anglatadi
auth={
"rotate_refresh_tokens": True,
"revoke_family_on_reuse": True,
"algorithm": "HS256", # or HS384/512, RS256, EdDSA
}
Har bir yangilash yangi token chiqaradi va taqdim etilganini bekor qiladi, shuning uchun o‘g‘irlangan yangilash tokeni aynan haqiqiy mijoz yangilash amalini bajargunga qadar foydali bo‘ladi — va shundan so‘ng ikkalasidan biri allaqachon ishlatilgan tokenni taqdim etadi.
Aynan shu qayta ishlatish (replay) — aniqlash usulidir. Uni haqiqiy mijoz bilan bir qatorda o‘g‘irlangan tokendan foydalanayotgan tajovuzkordan ajratib bo‘lmaydi, shuning uchun xavfsiz talqin — bu tajovuzkor bo‘lgan deb hisoblash, va butun oila (family) — bir xil kirishdan tarqalgan har bir token — bekor qilinadi. Qonuniy mijoz tizimdan chiqariladi va qayta autentifikatsiyadan o‘tishi kerak bo‘ladi. Bu haqiqiy xarajat, va u tajovuzkor jonli sessiyani ushlab turishiga qaraganda ancha kichikroqdir.
Aynan shuning uchun yangilash tomoni ombor (store)ga muhtoj, kirish tomoni esa emas. Kirish tokeni faqat o‘z imzosi orqali tekshiriladi — hech qanday qo‘shimcha so‘rov, workerlar orasida umumiy holat yo‘q — va u aynan qisqa muddatli, shuning uchun uni bekor qila olmaslik uzoq vaqt muhim bo‘lib qolmaydi.
RS256 va EdDSA tokenlarni boshqa xizmatda tekshiradigan joylashtirishlar
uchun mo‘ljallangan — bunda o‘sha xizmatga faqat ochiq kalit kerak bo‘ladi:
fort.configure_tokens(
signing_key=PRIVATE_KEY_PEM,
verifying_key=PUBLIC_KEY_PEM,
)
Maxfiy kalit cookie’larni imzolaydi va u assimetrik kalit emas, shuning uchun
juftliksiz RS256 so‘ralsa, xatolik birinchi token chiqarilganda emas,
balki konstruksiya bosqichidayoq yuzaga keladi.
So‘rovlar chastotasini cheklash
Sukut bo‘yicha yoqilgan va faqat admin panelning o‘z yo‘llari doirasida amal qiladi — o‘zi ulangan ilovaga so‘rovlarni rad eta boshlagan freymvork o‘z vazifasidan chetga chiqqan bo‘ladi.
rate_limit={
"read_per_minute": 600, # GET: lists, forms, the dashboard
"write_per_minute": 120, # everything that writes
"login_per_minute": 10, # POST to sign-in, right password or wrong
}
Bitta o‘rniga uchta byudjet — chunki admin panel xizmat ko‘rsatadigan uchta
narsa butunlay boshqa-boshqa qiymatga tushadi. Ro‘yxat sahifasi — bu so‘rov
(query); saqlash — bu tranzaksiya; kirish esa — bu Argon2 heshi, u
ataylab sekin qilingan, va aynan shu narsa kirish formasini istalgan
saytda hujum qilish uchun eng arzon narsaga aylantiradi. 100 ms davom
etadigan qilib sozlangan hesh degani — soniyasiga o‘n bitta
autentifikatsiyalanmagan so‘rov bitta yadroni to‘yintiradi, jo‘natuvchi esa
hech narsa sarflamaydi. Shuning uchun kirish byudjeti eng qattiq, va u
handler ishga tushishidan oldin, middleware’da hisoblanadi: rad etilgan
so‘rov heshlashgacha umuman yetib bormaydi. Token API ulangan bo‘lsa,
POST /auth/token ham xuddi shu byudjetdan hisoblanadi — bu boshqa eshik
ortidagi xuddi shu heshdir.
Sukut bo‘yicha qiymatlar ataylab bo‘sh (yumshoq) qilingan. Byudjet manzil bo‘yicha hisoblanadi, manzil esa inson emas: bitta NAT orqasidagi butun ofis — bu bitta manzil, va bitta insonning harakatiga moslab sozlangan chegara ro‘yxatni ochayotgan beshinchi hamkasbni allaqachon cheklab qo‘yadi. Soniyasiga barqaror o‘nta o‘qish — bir xona to‘la odam bosayotgan miqdordan ancha ko‘p, va o‘z javoblarini kutmaydigan scraper’dan esa ancha kam.
Rad etilgan so‘rov — bu Retry-After, X-RateLimit-Limit va
Cache-Control: no-store sarlavhalari bilan 429. Statik resurslar hech
qachon hisobga olinmaydi — bitta sahifa ularning bir nechtasini yuklaydi, va
ularni hisoblash bitta sahifa ko‘rishni oltita ko‘rishga aylantirar edi.
Bir nechta workerlar bilan yangilash ombori uchun bo‘lgan xuddi shu voqea — har biri hisoblagichlarning chorak qismini saqlaydigan to‘rtta jarayon jami cheklovni to‘rt baravar oshiradi:
fort.set_rate_limit_store(RedisRateLimitStore(...))
Proksi orqasida
Byudjet mijoz manziliga hisoblanadi, teskari proksi (reverse proxy) orqasida esa soket proksiga tegishli bo‘ladi — shuning uchun aytilmasa, cheklovchi butun trafikingizni bitta hisob (bucket)ga yozib boradi.
security={"forwarded_depth": 1} # one Caddy/nginx in front; 2 with a CDN too
X-Forwarded-For sarlavhasini so‘rovni jo‘natgan kishi yozadi, shuning uchun
uni o‘ngdan o‘qish kerak: chapda so‘rov jo‘natuvchi tanlagan qiymat
turadi, va har bir proksi undan keyin o‘zinikini qo‘shib boradi. O‘ngdan
sanash — tajovuzkor uzaytira olmaydigan yagona o‘qish usuli. Nol — sukut
bo‘yicha qiymat — sarlavhani butunlay e’tiborsiz qoldiradi va soketdan
foydalanadi, u esa har doim rostgo‘y va yagona kamchiligi shundaki, u
proksiga tegishli.
Uni juda yuqori qilib qo‘yish xavfli yo‘nalish: bunda mijoz o‘zi yozgan manzil o‘qiladi, va har bir so‘rov o‘zini yangi byudjetli va yangi bloklash hisoblagichli yangi mijoz sifatida ko‘rsatishi mumkin bo‘lib qoladi. Aynan shu sababdan bloklash va cheklovchi bitta shu funksiyani baham ko‘radi.
Darvoza
Admin panelning har bir marshruti router darajasidagi bitta bog‘liqlik (dependency) orqasida joylashgan, har bir view ichidagi tekshiruv orqali emas. Uni unutib qo‘yishi mumkin bo‘lgan birorta ham view yo‘q.
public aniq uchta narsani o‘z ichiga oladi: statik resurslar, kirish
sahifasi va til almashtirgich.
Mavjud bo‘lmagan qator har doim 404, hech qachon 403 emas. Yozuvni ko‘rishga ruxsati bo‘lmasligi mumkin bo‘lgan kishiga uning mavjudligini tasdiqlash — bu ma’lumot sizib chiqishi, shuning uchun ikkala javob ham bir-biridan farqlanmaydigan qilib qo‘yilgan.
CSRF
security={
"csrf_enabled": True,
"csrf_header_name": "X-CSRF-Token",
"csrf_field_name": "_csrf",
}
Har bir formada. Xuddi shu secret_key bilan imzolanadi, shuning uchun uni
almashtirish sessiyalar, CSRF-tokenlar va JWT’larni birgalikda amal
qilmaydigan holga keltiradi.
Cookie
security={
"cookie_name": "fastfort_session",
"cookie_secure": True,
"cookie_httponly": True,
"cookie_samesite": "lax",
}
Sukut bo‘yicha qiymatlar eng qat’iylaridir. Ulardan birortasini yumshatish
kod ko‘rib chiqishda (review) ko‘rinadigan aniq o‘zgarishni talab qiladi — va
baribir check --deploy bu haqda xabar beradi.
cookie_samesite="none" cookie_secure=True ni talab qiladi; sozlamalar
validatori bu kombinatsiyani ishlab chiqarishga chiqarishga yo‘l qo‘ymasdan,
to‘g‘ridan-to‘g‘ri rad etadi.
CSP
default-src 'none' va script-src 'self' bilan boshlanadi, nonce’siz.
Aynan shu yagona qaror front-endning shaklini asosan belgilab beradi:
- Hech qayerda inline
<script>yo‘q. - Inline hodisa ishlovchilari (event handler) yo‘q.
- CDN yo‘q. Ma’lumotlar brauzerga
data-atributlari orqali yetib boradi, ularni skript o‘qiydi.
Loyiha aniq ikkita direktivani kengaytira oladi, har birini aniq bitta origin bo‘yicha:
| Direktiva | Nima orqali kengaytiriladi |
|---|---|
img-src | ui.map_tile_url |
script-src | ui.richtext_url |
Ikkalasi ham shablon hook’lari emas, balki sozlamalar sifatida amalga oshirilgan — aynan siyosatni kengaytirish kimdir ko‘rib chiqadigan o‘zgarish bo‘lishi uchun.
Uning yonida: X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, va security.hsts_seconds o‘rnatilgan bo‘lsa — HSTS.
security={"security_headers": True, "hsts_seconds": 31_536_000}
Siqish va nima ataylab siqilmaydi
Statik resurslar uchun Brotli → gzip → identity kelishiladi, har bir kodlash jarayon (process) darajasida keshlanadi. HTML ataylab bundan chetlashtirilgan.
Sahifada bir vaqtning o‘zida CSRF-token va so‘rov tanlagan matn mavjud
bo‘ladi, bu esa aynan BREACH naqshi (pattern): ularni birga siqish matnga
ta’sir qila oladigan tajovuzkorga javob hajmi orqali tokenni bilib olish
imkonini beradi. Buni GZipMiddleware qo‘shish orqali «tuzatishga»
urinmang.
Yuklangan fayllar
Fayllar admin panel orqali, boshqa har qanday view kabi xuddi shu darvoza ortidan uzatiladi — alohida, autentifikatsiyasiz statik marshrut orqali emas. Yuklangan fayl — bu boshqa har qanday qator kabi yozuv, ochiq resurs emas.
media={"root": "media", "upload_limit": 10_000_000}
So‘rov yuklash tugaganini aniqlashdan oldin chegaradan ko‘pi bilan bitta bayt ortiqcha o‘qiydi, shuning uchun haddan tashqari katta fayl, avval to‘liq xotiraga joylashib, necha gigabayt bo‘lishidan qat’i nazar, maydondagi validatsiya xatosi bo‘lib qoladi.
Yuklash nimalardan o‘tishi kerak
Yuklangan fayllar /admin/media/… manzilidan qaytariladi — bu admin
panelning o‘z manbasi (origin), ya’ni sessiya cookie’si aynan shu manbaga
bog‘langan. Brauzerni hujjat sifatida bajarishga ko‘ndirish mumkin bo‘lgan
fayl shu sababli oddiy qiziqish emas; bu — uni ochgan kishi nomidan, uning
sessiyasi bilan ishlaydigan skriptdir. To‘rtta himoya bor, chunki ulardan
birortasi yolg‘iz qolsa ham, teshik ochiq qoladi.
Kengaytma ruxsat berilgan ro‘yxat (allow-list) bo‘yicha tekshiriladi, va alohida server yoki brauzer ishga tushirishi mumkin bo‘lgan hamma narsaning taqiqlangan ro‘yxati (deny-list)ga qarshi ham tekshiriladi:
media={
"allowed_extensions": {"pdf", "png", "jpg", "csv", "xlsx"},
"allowed_image_extensions": {"png", "jpg", "jpeg", "webp"},
}
Boshida nuqta bilan yoki nuqtasiz, istalgan registrda yozilishi mumkin. Rasm
maydoni torroq ro‘yxatni oladi, chunki photo — «rasm» ma’nosida va photo
— «zip-arxiv» ma’nosida, kichik rasmlar (thumbnail) ustuniga qarayotgan
kishi uchun ikki xil va’dadir.
Nom o‘rtasidagi xavfli kengaytma xavfli bo‘lishdan to‘xtaydi.
hack.exe.png — bu ba’zi serverlar chapdan o‘ngga o‘qib, interpretatorga
topshiradigan nom — klassik AddHandler xatosi — shuning uchun u
hack_exe.png sifatida saqlanadi, va unda endi .exe qolmaydi. Rad
etilmaydi, balki zararsizlantiriladi: ichida kengaytma yashiringan har
qanday nomni rad etish example.com.pdf faylini ham rad etadi, va oddiy
fayl uchun tushunarsiz xato beradigan qoida — bu odamlar aylanib o‘tadigan
qoidadir. archive.tar.gz esa tegilmagan holda qoladi, chunki tar — hech
kim ishga tushiradigan narsa emas.
Baytlar nom bilan solishtirib tekshiriladi. Kengaytma — bu faylni
yuklagan kishining shunchaki da’vosi. Boshlang‘ich magic-sonlar o‘qiladi, va
mazmuni ELF-binar, shell-skript yoki HTML hujjat bo‘lgan .png fayl mos
kelmaslik aniq ko‘rsatilgan holda rad etiladi — chunki qiziqarli hujum aynan
ikkalasi bir-biriga mos kelmaydigan holatdir.
Hech narsa ishonch asosida uzatilmaydi. Javobning Content-Typei
saqlangan nom emas, balki saqlangan baytlar asosida belgilanadi, va aniq
ravishda rastr tasvir sifatida tanilmagan har qanday narsa text/plain
sifatida va yuklab olish (download) sifatida beriladi. Bu yuqoridagi
mexanizmlar mavjud bo‘lmasdan oldin yozilgan yoki media katalogiga
FastFort’dan boshqa narsa tomonidan joylashtirilgan fayllarni ham qamrab
oladi.
Kirish yozuvlari
Kim kirdi, qayerdan va nimadan. Loyihangiz egalik qiladigan jadvalga qarshi bitta chaqiruv:
from fastfort.orm.sqlalchemy import SignInRecordMixin
class SignInRecord(SignInRecordMixin, Base):
__tablename__ = "admin_sign_in"
fort.record_sign_ins(SignInRecord)
Har bir urinish bitta qatorga aylanadi: qachon, qanday identifikator kiritilgani, muvaffaqiyatli bo‘lgan-bo‘lmagani, manzil, brauzer, platforma, qurilma turi va xom user-agent qatori.
FastFort hech qanday migratsiya yetkazib bermaydi va hech qanday jadval
yaratmaydi, shu sababli model — sizniki: mixin faqat ustunlarni taqdim
etadi, kamroq narsa xohlagan loyiha esa kamroq e’lon qiladi. Faqat at,
successful va address majburiy, va ustunlar record_sign_ins
chaqirilganda tekshiriladi, shuning uchun xato yozilgan sxema tunning
uchida kutilmagan hodisa emas, balki ilova ishga tushishidagi xatodir.
To‘rtta qarorni bilib olish kerak, chunki ularning har biri aniq-ravshan yechimning aksidir.
Avval client hints, so‘ngra user-agent. Chromium’ga asoslangan
brauzerlar so‘ralmasdan Sec-CH-UA, Sec-CH-UA-Platform va
Sec-CH-UA-Mobile sarlavhalarini jo‘natadi, va bular strukturaviy
maydonlardir; user-agent esa 1994 yildan beri kengayib kelayotgan bir
jumladir. Xom qator baribir tanilgan qiymat yonida saqlanadi, chunki unda
har bir brauzer yolg‘on gapiradi — Chrome o‘zini Safari deb, Safari esa
o‘zini Gecko deb ataydi — va tanilgan qiymat noto‘g‘ri bo‘lishi mumkin.
FastFort ichida hech narsa bularning hech biriga asoslanib qaror qabul
qilmaydi: user-agent’ni so‘rovni jo‘natgan kishi yozadi, shuning uchun bu
inson o‘qishi uchun dalil, hech qachon qaror uchun kirish ma’lumoti emas.
Muvaffaqiyatsiz urinishlar ham qayd etiladi, agar siz failures=False
deb bermasangiz. Faqat muvaffaqiyatli kirishlar jurnali kimdir to‘rt yuzta
parolni sinab ko‘rgan tun haqida hech narsa aytmaydi.
Foydalanuvchilar jadvaliga tashqi kalit (foreign key) yo‘q. Hisob matn sifatida saqlanadi — identifikator kiritilgan holicha, va asosiy kalit (primary key) satr sifatida. Hisob bilan birga kaskad tarzda o‘chib ketadigan audit qatori — audit qatori emas, va «bu hisobni kim, qayerdan o‘chirdi» — aynan shu yozuvlar javob berish uchun mavjud.
Yozuvni yozish hech qachon kirishning muvaffaqiyatsiz bo‘lishiga sabab
bo‘la olmaydi. U o‘zining alohida tranzaksiyasida ishlaydi, va xatolik
chiqarilmaydi (raise), balki fastfort.auth orqali jurnalga yoziladi: yo‘q
yoki to‘lib ketgan audit jadvali buni tuzata oladigan odamlarni tizimdan
chetlab qo‘ya olmasligi kerak.
Manzil qayerdan aniqlanadi
fort.record_sign_ins(SignInRecord, locate=my_geoip_lookup)
FastFort hech qanday GeoIP bazasini o‘z ichiga olmaydi va hech qanday
xizmatga murojaat qilmaydi. Birinchisi wheel-paket ichida eskirib boradigan
yuz megabaytni anglatgan bo‘lardi; ikkinchisi esa har bir administratorning
manzilini kirish handleri ichidan uchinchi tomonga uzatgan bo‘lardi. Agar
sizda o‘z bazangiz bo‘lsa, locate — manzilni qabul qilib, satr
qaytaradigan chaqiriladigan obyekt, va xatolik chiqaradigan callback faqat
manzilni aniqlay olmaslikka arzon tushadi, boshqa hech narsaga emas.
Ularni o‘qish
SignInRecord — oddiy model, shuning uchun uni boshqa har qanday model
kabi ro‘yxatdan o‘tkazing va bepul filtrlanadigan, qidirilishi mumkin
bo‘lgan, eksport qilinadigan sahifaga ega bo‘ling. kind — belgilangan
qiymatlar to‘plami: desktop, mobile, tablet, bot, unknown —
shuning uchun u hech kim guruhlay olmaydigan ustun emas, balki filtr va
Breakdown bo‘la oladi.
Admin hisobga nima qila oladi
«Administrator hamma narsani qila oladi» degan sukut bo‘yicha qiymat noto‘g‘ri bo‘lgan joylashtirishlar uchun to‘rtta sozlama — ochiq demo, umumiy sandbox, production ma’lumotlarining staging nusxasi:
FastFortSettings(
auth={
"allow_password_change": False,
"allow_superuser_password_change": False,
"allow_user_delete": False,
"allow_superuser_delete": False,
},
)
| Sozlama | Sukut bo‘yicha | Nimani to‘xtatadi |
|---|---|---|
allow_password_change | True | Har qanday hisobning parolini admin panel orqali o‘zgartirish |
allow_superuser_password_change | True | Superfoydalanuvchi parolini boshqa kimdir tomonidan o‘zgartirish |
allow_user_delete | True | Har qanday hisobni o‘chirish |
allow_superuser_delete | True | Superfoydalanuvchini o‘chirish |
Ular faqat foydalanuvchi modeliga tegishli, boshqa hech narsaga emas, va barcha administratorlarga bab-baravar qo‘llaniladi — ularni yozgan kishiga ham. Bularning hech biri huquqlar tizimi emas. Har bir kishi uchun farq qiladigan qoidalar — bu avtorizatsiya, u sizniki va darvozaga ulanadi.
Himoyalangan parol maydoni faqat o‘qish uchun ko‘rsatiladi va yozishdan chiqarib tashlanadi, chunki yuborilgan qiymatni baribir qabul qiladigan «faqat o‘qish uchun» boshqaruv elementi — himoya emas, balki yorliqdir. Himoyalangan hisob tasdiqlash sahifasida va bosilgan payt qayta rad etiladi, chunki o‘sha sahifa bir soatdan beri ochiq turgan bo‘lishi mumkin. Ommaviy o‘chirish himoyalangan qatorlarni saqlab qoladi, qolganlarini o‘chiradi va nechtasi saqlanganini bildiradi.
Superfoydalanuvchi har doim o‘z parolini o‘zgartira oladi: odamlarni o‘zlarining hisob ma’lumotlaridan mahrum qilish — bu boshqa funksiyadir.
Ochiq demo’da superfoydalanuvchi varianti o‘rniga allow_password_changedan
foydalaning. Har bir tashrif buyuruvchini bir xil hisob ostida kiritadigan
demo ularning har birini o‘sha hisobga aylantiradi, va torroq sozlama
ataylab o‘z parolini o‘zgartirishga hali ham ruxsat berib qo‘yadi.
Joylashtirishdan oldin
uv run fastfort check --app main:fort --deploy
Yoki ilova ishga tushishini rad etadigan qilib sozlang:
settings.require_production_ready()