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.

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:

DirektivaNima orqali kengaytiriladi
img-srcui.map_tile_url
script-srcui.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,
    },
)
SozlamaSukut bo‘yichaNimani to‘xtatadi
allow_password_changeTrueHar qanday hisobning parolini admin panel orqali o‘zgartirish
allow_superuser_password_changeTrueSuperfoydalanuvchi parolini boshqa kimdir tomonidan o‘zgartirish
allow_user_deleteTrueHar qanday hisobni o‘chirish
allow_superuser_deleteTrueSuperfoydalanuvchini 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()