Bölüm Müdürü Sadece Kendi Bölümünü Görsün: SQLAlchemy ile Otomatik Yetki Filtresi
Her sorguya elle bölüm koşulu yazmanın neden tehlikeli olduğunu, SQLAlchemy 2'de oturum seviyesinde otomatik filtre uygulamanın nasıl yapıldığını, test yöntemini ve sınırlarını anlatan proje notu.
Çok kullanıcılı bir panelde "kim neyi görebilir?" sorusunun en tehlikeli cevabı şudur: "Her sorguya WHERE bolum = ... yazıyoruz." Çünkü bir gün biri yeni bir rapor ekler ve bu koşulu unutur; kimse fark etmez, ta ki bir müdür başka bölümün maaş ya da devamsızlık verisini görene kadar. Bu not, bir otel personel panelinde bölüm müdürlerinin yalnızca kendi bölümlerini görmesini unutulamaz hale getirmek için ne yaptığımızı anlatıyor.
Gereksinim
Üç rol vardı: yönetici (hepsi), insan kaynakları (hepsi, salt okunur) ve bölüm müdürü (yalnızca kendi bölümü). Müdürün hesabı bir bölüme bağlıydı; müdür personel, hareket, izin ve rapor sayfalarının hepsinde yalnızca o bölümün verisini görmeliydi. Genel güvenlik mimarisi için güvenlik katmanları notuna bakın.
Yanlış yaklaşım: elle filtre
# Her sorguda tekrar eden, unutulabilir kod
q = Personel.query
if user.rol == "mudur":
q = q.filter(Personel.bolum_id == user.bolum_id)
Bu kod on sayfada çalışır, on birincide unutulur. Ayrıca ilişkili tablolar da (hareket, izin) personele bağlı olduğundan, her birinde ayrı koşul gerekir; birinde yanlış birleştirme yapılırsa sızıntı olur. Test bile yakalamaz, çünkü testler genelde yönetici hesabıyla yazılır.
Doğru yaklaşım: oturum seviyesinde filtre
SQLAlchemy 2'de her SELECT için çalışan bir olay (do_orm_execute) ve bir varlığa otomatik koşul ekleyen with_loader_criteria vardır. Mantık: oturum açık kullanıcı müdürse, her sorguya o bölüme ait koşul otomatik eklenir.
from flask import session
from sqlalchemy import event
from sqlalchemy.orm import Session, with_loader_criteria
@event.listens_for(Session, "do_orm_execute")
def bolum_filtresi(state):
if not state.is_select:
return
if session.get("rol") != "mudur":
return
bolum = session.get("bolum_id")
state.statement = state.statement.options(
with_loader_criteria(Personel, Personel.bolum_id == bolum, include_aliases=True)
)
Hareket ve izin gibi personele bağlı tablolar için ya doğrudan o tabloda bölüm sütunu tutulur ya da aynı mantık her varlık için tanımlanır. En sağlam yöntem, yetki sınırının bir sütun olarak her varlıkta bulunmasıdır; birleştirmelerle uğraşmak yerine her tablo kendi bölümünü bilir.
İkinci güvence: sayfa izin listesi
Veri filtresi güçlü ama yeterli değil: müdürün hiç görmemesi gereken sayfalar da vardır (ayarlar, kullanıcı yönetimi). Bunun için bir izin listesi (whitelist) kullandık: müdürün açabileceği sayfa adları açıkça yazıldı; listede olmayan her sayfa kapalıdır.
MUDUR_IZINLI = {"index", "personel_listesi", "hareketler", "aylik_ozet"}
@app.before_request
def izin_kontrol():
if session.get("rol") == "mudur" and request.endpoint not in MUDUR_IZINLI | {"static", "cikis"}:
abort(403)
Bu "önce kapalı" düzeni, yeni bir sayfa eklendiğinde varsayılan olarak müdüre kapalı olmasını sağlar. Bir kez yeni bir Excel indirmesini listeye eklemeyi unuttuk ve müdürler "erişim yok" gördü; sinir bozucu ama güvenli bir hataydı.
Doğrudan nesne erişimi
/personel/42/gecmis gibi adreslerde kimliği değiştirerek başka bölümün personeline ulaşma denemesi (IDOR) otomatik filtre sayesinde "kayıt bulunamadı" ile sonuçlanır; çünkü get(42) sorgusu da filtreden geçer. Sayfa 403 yerine 404 döndürürse, kaydın var olup olmadığı bile sızmaz.
Test yöntemi
Filtrenin işe yaradığını görmek için müdür gözüyle test yazın:
- Test veritabanında iki bölüm ve her birinde birkaç personel oluşturun.
- İki farklı müdür hesabıyla oturum açıp her sayfayı çağırın ve dönen HTML'de diğer bölümün adlarının geçmediğini doğrulayın.
- Yönetici hesabıyla aynı sayfalarda toplam sayıların büyük olduğunu kontrol edin.
- Başka bölümün kayıt kimliğini adres satırına yazıp 404 aldığınızı doğrulayın.
Bu testi her yeni sayfa eklediğimizde çalıştırdık; birkaç kez ham SQL ile yazılmış bir sorgunun filtreyi atladığını böyle yakaladık (ham text() sorguları ORM olayını tetiklemez, onlara elle koşul gerekir).
Sınırlar
- Ham SQL filtreyi atlar:
text()ile yazılan sorgularwith_loader_criteriakapsamında değildir; bunlardan kaçının ya da kod incelemesinde özellikle arayın. - Toplamlar: Müdüre gösterilen genel toplamlar (örneğin otel geneli) başka bölümün verisini ima edebilir; hangi toplamların gösterileceğine bilinçli karar verin.
- Önbellek: Sonuçları kullanıcıdan bağımsız önbelleğe almak, bir müdürün verisini diğerine gösterebilir. Önbellek anahtarına rolü ve bölümü katın.
- Arka plan işleri: Oturum dışı çalışan betikler (özet e-postaları) kendi yetki mantığını ayrıca uygulamalıdır (günlük özet e-postası).
Çıkardığımız ders
Güvenlik kuralını geliştiricinin hatırlamasına bırakmayın; sistemin kendisi uygulasın. Varsayılan kapalı, istisna açık. Ve her yeni özelliği, yetkisi en düşük hesapla test edin. Tüm mimari kararların bağlamı için proje mimarisi notuna göz atabilirsiniz.