Ders 22 / 23
Çok Kiracılı Yetkilendirme
Kiracı sınırının sorguya taşınması, süzgeç unutulduğunda oluşan sızıntının ölçülmesi, kiracı kimliğinin kimliği doğrulanmış bağlamdan alınması, üç katmanlı savunma ve fiziksel yerleşim seçenekleri.
İçindekiler
Önceki ders kapsamı bir istemcinin yetkisini daraltmak için kullandı. Bütün örneklerde arka planda tek bir kütüphane vardı: bir üye kümesi, bir görevli kadrosu, bir kayıt havuzu. Aynı sunucu birden çok kuruma hizmet verdiğinde araya yeni bir sınır girer.
Bu sınırın önceki modellerden farkı, veriyle çizilmesidir. Kuzey Kütüphanesi’nin görevlisi kendi kurumunun bütün kayıtlarını görebilir; Güney Kütüphanesi’nin aynı roldeki görevlisi de kendi kurumunun kayıtlarını görebilir. İkisinin rolü, izinleri ve belirteç kapsamları aynıdır. Ayıran tek şey, sorgunun hangi satırlara dokunduğudur.
Yalıtım Rolle Değil Sorguyla Kurulur
Çok kiracılı bir sistemde her kayıt bir kiracıya (tenant) aittir. Kiracı, verinin sahibi olan kurumdur ve şemada bir sütun olarak durur. Yetki denetimi bu sütuna bakmadığında rol denetimi geçen bir istek, komşu kurumun kaydını döndürür.
Aşağıdaki hesap iki depo gerçekleştirimini aynı veri üzerinde çalıştırır: biri kiracı süzgecini unutmuş, diğeri her sorguda zorunlu tutuyor.
cat > kiraci.mjs <<'EOF' // kiraci.mjs — kiraci yalitiminin sorguya tasinmasi ve denetlenmesi import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync(":memory:"); db.exec(` CREATE TABLE uye (uye_id TEXT PRIMARY KEY, kiraci TEXT NOT NULL, ad TEXT NOT NULL); CREATE TABLE odunc (odunc_id TEXT PRIMARY KEY, kiraci TEXT NOT NULL, uye_id TEXT NOT NULL, isbn TEXT NOT NULL, durum TEXT NOT NULL); INSERT INTO uye VALUES ('U-1','kuzey','Ayse Yilmaz'), ('U-2','kuzey','Deniz Kaya'), ('U-9','guney','Selin Aydin'); INSERT INTO odunc VALUES ('O-1','kuzey','U-1','978-0201896831','ACIK'), ('O-2','kuzey','U-2','978-0262033848','KAPALI'), ('O-7','guney','U-9','978-0131103627','ACIK'), ('O-8','guney','U-9','978-0201896831','ACIK'); `); // 1. Kiraci suzgeci unutulmus depo: yalnizca durum kosulu var. const gevsek = { ad: "kiraci suzgeci yok", acikOduncler: () => db.prepare("SELECT odunc_id, kiraci FROM odunc WHERE durum = 'ACIK'").all(), uyeBul: (id) => db.prepare("SELECT uye_id, kiraci, ad FROM uye WHERE uye_id = ?").all(id), }; // 2. Kiraci suzgeci zorunlu depo: her sorgu baglamdaki kiraciyi alir. const siki = { ad: "kiraci suzgeci zorunlu", acikOduncler: (k) => db.prepare("SELECT odunc_id, kiraci FROM odunc WHERE kiraci = ? AND durum = 'ACIK'").all(k), uyeBul: (k, id) => db.prepare("SELECT uye_id, kiraci, ad FROM uye WHERE kiraci = ? AND uye_id = ?").all(k, id), }; const KIRACI = "kuzey"; // istegi yapan gorevlinin kiracisi const sizan = (satirlar) => satirlar.filter((s) => s.kiraci !== KIRACI).length; console.log("depo sorgu donen sizan"); for (const [depo, satirlar] of [ [gevsek, gevsek.acikOduncler()], [siki, siki.acikOduncler(KIRACI)], ]) { console.log(`${depo.ad.padEnd(23)} ${"acikOduncler".padEnd(17)} ${String(satirlar.length).padStart(5)} ${String(sizan(satirlar)).padStart(6)}`); } for (const [depo, satirlar] of [ [gevsek, gevsek.uyeBul("U-9")], [siki, siki.uyeBul(KIRACI, "U-9")], ]) { console.log(`${depo.ad.padEnd(23)} ${"uyeBul('U-9')".padEnd(17)} ${String(satirlar.length).padStart(5)} ${String(sizan(satirlar)).padStart(6)}`); } // 3. Kiracinin kaynagi: istekten mi, belirtecten mi? function baglamKur(kaynak, belirtec, istek) { return kaynak === "istek" ? istek.kiraci : belirtec.kiraci; } const belirtec = { ozne: "P-2001", kiraci: "kuzey" }; console.log("\nkiraci kaynagi istekteki deger kullanilan donen kayit sizan"); for (const kaynak of ["istek", "belirtec"]) { const istek = { kiraci: "guney", uye_id: "U-9" }; // istemci baska kiraci yaziyor const k = baglamKur(kaynak, belirtec, istek); const satirlar = siki.uyeBul(k, istek.uye_id); console.log(`${kaynak.padEnd(16)} ${istek.kiraci.padEnd(16)} ${k.padEnd(11)} ${String(satirlar.length).padStart(11)} ${String(sizan(satirlar)).padStart(6)}`); } // 4. Statik denetim: kiraci sutunu tasiyan tablolara giden sorgularda suzgec var mi? const KIRACILI_TABLOLAR = ["uye", "odunc"]; const SORGULAR = [ { ad: "acikOduncler/gevsek", sql: "SELECT odunc_id FROM odunc WHERE durum = 'ACIK'" }, { ad: "acikOduncler/siki", sql: "SELECT odunc_id FROM odunc WHERE kiraci = ? AND durum = 'ACIK'" }, { ad: "uyeBul/siki", sql: "SELECT ad FROM uye WHERE kiraci = ? AND uye_id = ?" }, { ad: "sayim", sql: "SELECT count(*) FROM odunc" }, { ad: "kitap listesi", sql: "SELECT isbn FROM kitap" }, ]; console.log("\nsorgu tablo kiracili suzgec bulgu"); let bulgu = 0; for (const s of SORGULAR) { const tablo = /FROM\s+(\w+)/i.exec(s.sql)?.[1] ?? "-"; const kiracili = KIRACILI_TABLOLAR.includes(tablo); const suzgec = /\bkiraci\s*=\s*\?/i.test(s.sql); const sorunlu = kiracili && !suzgec; if (sorunlu) bulgu++; console.log(`${s.ad.padEnd(22)} ${tablo.padEnd(7)} ${(kiracili ? "evet" : "hayir").padEnd(9)} ${(suzgec ? "var" : "yok").padEnd(7)} ${sorunlu ? "EKSIK SUZGEC" : "-"}`); } console.log(`denetim bulgusu: ${bulgu} / ${SORGULAR.length}`); EOF node kiraci.mjs
depo sorgu donen sizan
kiraci suzgeci yok acikOduncler 3 2
kiraci suzgeci zorunlu acikOduncler 1 0
kiraci suzgeci yok uyeBul('U-9') 1 1
kiraci suzgeci zorunlu uyeBul('U-9') 0 0
kiraci kaynagi istekteki deger kullanilan donen kayit sizan
istek guney guney 1 1
belirtec guney kuzey 0 0
sorgu tablo kiracili suzgec bulgu
acikOduncler/gevsek odunc evet yok EKSIK SUZGEC
acikOduncler/siki odunc evet var -
uyeBul/siki uye evet var -
sayim odunc evet yok EKSIK SUZGEC
kitap listesi kitap hayir yok -
denetim bulgusu: 2 / 5
Unutulan Süzgecin Gözlenebilir Sonucu
Çıktının ilk bölümü sızıntıyı sayıyor. Süzgeçsiz depo, açık ödünçler sorgusunda üç kayıt döndürüyor ve bunların ikisi başka kiracıya ait. Üye arama sorgusu daha çarpıcıdır: istenen kimlik komşu kurumun üyesine aittir ve süzgeçsiz sorgu o kaydı döndürür — rol denetimi geçilmiştir, çünkü görevlinin “üye okuma” izni vardır. İzin doğrudur; yanlış olan, iznin hangi satırlar üzerinde geçerli olduğunun sorulmamasıdır.
Süzgeçli depoda aynı iki sorgu sıfır sızıntı veriyor. İkinci sorgunun sonucu boştur: kayıt vardır ama bu kiracı için yoktur. Boş sonuç ile yetkisizlik yanıtı arasındaki seçim bir tasarım kararıdır ve genellikle boş sonuç tercih edilir; “bu kimlik başka bir kurumda var” bilgisi bile kiracılar arası bir sızıntıdır.
Kiracı Bilgisi Nereden Gelir
İkinci bölüm daha ince bir hatayı gösteriyor. Kiracı kimliği isteğin gövdesinden ya da yol
parametresinden okunursa, istemci onu değiştirebilir: istek guney yazar, sorgu o kiracıyı
kullanır ve komşu kurumun kaydı döner. Aynı istek, kiracı kimliği belirteçten alındığında
sıfır kayıt döndürür.
Kural tek cümledir: kiracı kimliği, kimliği doğrulanmış bağlamdan gelir; istekten gelen kiracı değeri en fazla bir filtre olabilir, hiçbir zaman yetkinin kaynağı olamaz. İstemcinin gönderdiği kiracı ile bağlamdaki kiracı çelişiyorsa istek reddedilir; bu çelişki aynı zamanda kaydedilmeye değer bir olaydır.
Süzgeci Unutmayı Zorlaştırmak
Üçüncü bölüm bir denetim yazıyor: kiracı sütunu taşıyan tablolara giden sorgular süzgeci içeriyor mu? Modeldeki beş sorgunun ikisi bulgu üretti — biri bilinçli olarak gevşek bırakılan depo, diğeri masum görünen bir sayım sorgusu. Sayım sorgusu tek bir sayı döndürür ama o sayı bütün kiracıların toplamıdır; gösterge panosunda bu, komşu kurumun iş hacmini sızdırır.
Denetimin değeri, unutmayı yapısal olarak yakalamasıdır. Aynı işi üç katman birlikte yapar ve her biri diğerinin kaçırdığını tutar:
- Depo katmanı: kiracı parametresi olmayan bir sorgu yazılamaz — sorguyu kuran işlev kiracıyı zorunlu argüman olarak alır.
- Denetim betiği: yeni yazılan sorgular sürekli tümleştirmede taranır, süzgeçsiz sorgu derlemeyi düşürür.
- Veritabanı katmanı: satır düzeyi güvenlik politikası, uygulama katmanı hata yapsa bile satırları süzer. Bu, İlişkisel Veritabanı Yönetimi kursundaki Satır Düzeyi Güvenlik dersinin uygulama tarafındaki karşılığıdır.
Üçü birden gerekir. Yalnız depo katmanına güvenmek, ham sorgu yazan bir raporlama betiğinde kırılır; yalnız veritabanı katmanına güvenmek, kiracı bağlamını oturuma bağlamayan bir toplu işte kırılır.
Kiracı Verisinin Fiziksel Yerleşimi
Yalıtımın üç yerleşim biçimi vardır ve seçim ölçütü aynı üç soruyla verilir: komşu kiracının verisine yanlışlıkla erişme olasılığı, kiracı başına maliyet ve göç ile yedeklemenin tanecikliği.
Paylaşılan tablo (bu dersteki model) en ucuzudur ve en çok özen ister: bütün kayıtlar aynı tablodadır, yalıtımı sorgu sağlar. Kiracı başına şema, süzgeci bağlantı düzeyine taşır; yanlış şemaya bağlanmak hâlâ olanaklıdır ama sorgu içindeki unutkanlık zararsızlaşır. Kiracı başına veritabanı en güçlü yalıtımı verir; bedeli, göç ve yedekleme işlerinin kiracı sayısıyla çarpılmasıdır.
Karar tek seferlik değildir: küçük kiracılar paylaşılan tabloda, sözleşmesi yalıtım gerektiren büyük kiracılar ayrı veritabanında tutulabilir. Bu durumda uygulama katmanının her iki yerleşimi de aynı arayüzün arkasında görmesi gerekir.
Özet
- Çok kiracılı sistemde sınır rol ya da kapsamla değil veriyle çizilir; aynı role sahip iki görevli farklı satır kümelerini görür.
- Kiracı süzgeci unutulduğunda rol denetimi geçen istek komşu kurumun kaydını döndürür; modeldeki iki sorgu üç kayıttan ikisini sızdırdı, süzgeçli sürüm sıfır sızdırdı.
- Kiracı kimliği kimliği doğrulanmış bağlamdan gelir; istekten gelen değer yetkinin kaynağı olamaz — modelde istekten okunan kiracı komşu kurumun kaydını açtı.
- Süzgeci unutmak üç katmanla zorlaştırılır: kiracıyı zorunlu argüman yapan depo, sorguları tarayan denetim ve satır düzeyi güvenlik politikası.
- Fiziksel yerleşim (paylaşılan tablo, kiracı başına şema, kiracı başına veritabanı) yalıtım gücü ile kiracı başına maliyet arasındaki ödünleşimdir.
Sonraki Adım
Kiracı sınırı, isteğin hangi kümeye dokunabileceğini belirledi. Küme içinde ise hâlâ tek tek nesneler vardır: aynı kütüphanenin iki üyesi, birbirinin ödünç kaydını görmemelidir. Kursun son dersi bu son sınırı ele alır — adres çubuğuna başka bir kimlik yazıldığında ne olduğunu ve nesne sahipliğinin nerede denetlenmesi gerektiğini.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.