Teknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor HaberleriTeknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor HaberleriTeknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor Haberleri
Yazı Tipi BoyutlandırıcıAa
  • Anasayfa
  • Teknoloji
    • Siber Güvenlik
    • Yapay Zeka
    • Donanım
    • Bilim
  • Yazılım
  • Savunma & İstihbarat
  • Oyun
  • Yaşam
    • Finans
    • Sinema
    • Dünyadan Haberler
  • İş Birliği
Okuma: Canlı CRM’den Çoklu Kiracılık ve İkinci İncelemenin Bulduğu İki Eksiklik
Paylaş
Yazı Tipi BoyutlandırıcıAa
Teknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor HaberleriTeknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor Haberleri
Ara
Bizi Takip Et
  • Hakkımızda
  • Gizlilik politikası
  • Tanıtım Yazısı ve Backlink Hizmeti
© 2026 Teknomers. All Rights Reserved.

Anasayfa » Canlı CRM’den Çoklu Kiracılık ve İkinci İncelemenin Bulduğu İki Eksiklik

Yazılım

Canlı CRM’den Çoklu Kiracılık ve İkinci İncelemenin Bulduğu İki Eksiklik

teknomers
Son güncelleme: 4 Haziran 2026 06:11
teknomers
Paylaş
Paylaş

Son gönderdiğim v0.2.x sürümüyle LaraFoundry üzerinde kimlik doğrulama ve kullanıcı yönetimi işlevselliğini oluşturmuştum. Bu yazı ise v0.3.0 ve bir SaaS uygulamasının önceki adımlarının mantıklı olması için ihtiyaç duyduğu şey: çoklu kiralama. Şirketler, ekipler, kim neyi görebilir.

Seriyi takip ettiyseniz, artık bu yazımın bir kalıp olduğunu biliyorsunuzdur. Bunu sıfırdan yazmıyorum. Üretimde yıllar boyunca çalışan bir CRM’den çıkararak, eski kodu yeniden gözden geçirip, paket içine modernleştirerek taşıyorum. Çoklu kiralama modülü, bu yeniden gözden geçirmenin en fazla karşılığını aldığı alan oldu; çünkü orijinalinde verilerin ayrılması ile ilgili bir hata vardı ve bu durum sessizce orada kalmıştı.

Sonrasında, kendime güvenerek kod incelememi yaptım, “12 bulgunun 8’ini hallettim,” dedim, kendimi iyi hissettim ve neredeyse gönderim yapacaktım. Ardından ikinci bir inceleme yaptım. İkinci tur, ilkinde gözden kaçan iki güvenlik açığı buldu. Her ikisi de gerçekti. Her biri, bir kiracının diğer kiracının verilerine erişmesini sağlıyordu. Bu konuda daha fazla bilgi daha sonra, bu yüzden bu yazıyı yazıyorum.


Buradaki Çoklu Kiralama Anlamı

Bir veritabanası, birçok kiracı, satır düzeyinde izolasyon. Her kiracı için ayrı veritabanı yok, her kiracı için ayrı şema yok. Her kiracıya ait satır, bir company_id taşır ve global bir sorgu kapsamı, yalnızca mevcut olduğunuz şirketin satırlarını görmenizi sağlamak için her zaman kontrol eder.

Temel yapı, iki modu da bir yapılandırma anahtarı aracılığıyla gönderir:

  • teams – kiracı bir Şirket. Kullanıcılar bir veya daha fazla şirkete ait olabilir, bunlar arasında geçiş yapabilir ve birbirlerini davet edebilirler. Benim CRM’imin kullandığı budur.
  • personal – kiracı Kullanıcıdır. Hiçbir şirkete ait yoktur, her kullanıcı kendi adasıdır. company_id yerine user_id ile filtrelenir, tek kişilik sahte bir şirket yoktur.

Ev sahibi, yapılandırmada modu seçer ve geri kalan paket buna göre uyum sağlar. Aynı kapsam, aynı trait, farklı sütun.


Önemli Karar: Fail-Closed

İşte eski kod, kabaca, canlı CRM’den:

// eski küresel kapsam, basitleştirilmiş
public function apply(Builder $builder, Model $model)
{
    $companyId = session('active_company_id');

    if ($companyId) {
        $builder->where('company_id', $companyId);
    }
    // aksi takdirde: filtre yok. tüm satırlar.
}

O else ifadesini tekrar okuyun. Eğer oturumda aktif bir şirket yoksa, kapsam hiçbir filtre eklemez ve sorgu, tüm kiracıların tabloyu sorgulamasına olanak tanır.

Normal kullanımda her zaman aktif bir şirket vardır, bu nedenle bu durum asla tetiklenmez. Ancak “normal kullanım” bir güvenlik sınırı değildir. Arka plan görevi, bir konsol komutu, aktif şirket belirlenmeden modeli vuran bir istek, talep sırasında oturumun sona ermesi gibi durumlar bunu sağlıyor. Herhangi birinde $companyId = null olur ve şimdi kiracı kapsamı geniş açılır. Bu, bir ders kitabı tanımlayıcıdan dışarı çıkmış durum.

Paket sürümü bunu tersine çevirir. Çözümlenemeyen kiracı yoksa, satır yok demektir, tüm satırlar yok:

public function apply(Builder $builder, Model $model): void
{
    $tenantKey = static::currentTenantKey();

    if ($tenantKey === null) {
        // Fail closed. Çözülmemiş kiracı -> hiç bir şey gösterme, asla her şeyi gösterme.
        $builder->whereRaw('0 = 1');

        return;
    }

    $builder->where(static::tenantColumn(), $tenantKey);
}

Bir sorun çıkarsa, boş bir liste görüyorsunuz. Bu can sıkıcı. Eski davranış bir veri sızıntısıydı. Can sıkıcı olmaya tercih ederim.

Aynı ilke yazma yolunda da geçerli. Kiracı sahipli bir model oluşturduğunuzda, company_id otomatik olarak çözücüden doldurulur ve kütle-atanabilir değildir, böylece oluşturulmuş bir form yüklemesi, bunu başka birinin şirketine ayarlayamaz. Ve yaratım zamanında kiracı çözülmemişse, yetim bir satır yazmadığı için hata fırlatır:

static::creating(function (Model $model) {
    $tenantKey = static::currentTenantKey();

    if ($tenantKey === null) {
        throw new RuntimeException('Aktif bir kiracı olmadan kiracıya ait bir model oluşturulamaz.');
    }

    $model->setAttribute(static::tenantColumn(), $tenantKey);
});


Çözücü ve Neden Bir Arayüzün Ardında

“Şu anda hangi şirket olarak hareket ediyorum?” sorusu, bulunduğunuz yere bağlı olarak birden fazla yanıtı olan bir sorudur. Bir tarayıcı oturumunda, her cihazda saklanır. Bir API tokeni aracılığıyla (bu, mobil uygulamada gelecek bir aşama) token’dan türetilecektir. Kiracı kapsamının bunun hakkında bilgisi yoktur veya önemi yoktur.

Bu nedenle, aktif kiracı bir TenantResolver sözleşmesinden gelir:

interface TenantResolver
{
    public function current(Authenticatable $user): ?Tenant;
    public function setCurrent(Authenticatable $user, Tenant|int|null $tenant): void;
    public function forget(Authenticatable $user): void;
}

teams modunda bağlama, user_sessions.active_company_id‘yı okuyup yazan bir SessionTenantResolverdır (aktif şirket, oturum satırında kimlik doğrulama aşamasından izlenir). personal modunda ise, sadece kullanıcıyı döndüren bir çözücü vardır. Daha sonra API aşaması burada bir token çözücüsü ekleyecek ve başka hiçbir şey değişmeyecek. Kapsam, trait, denetleyiciler, hepsi arayüze hitap etmeye devam eder.


Kullanıcı Tarafı Bir Trait’tir, Temel Sınıf Değil

Ev sahibinin Kullanıcı modeli, önceki aşamadan kimlik doğrulama trait’ini zaten kullanıyor. Kiralama, yanına oturan ikinci bir trait’tir:

class User extends Authenticatable
{
    use IsLaraFoundryUser;   // aşama 1.1: kimlik, oturumlar, 2FA
    use BelongsToTenancy;    // aşama 1.2: şirketler, aktif şirket, sahip kontrolü
}

BelongsToTenancy kullanıcının companies(), getActiveCompany(), setActiveCompany(), isOwnerOf() gibi metotlara ve şirket geçişinin ihtiyaç duyduğu bileşenlere erişim sağlar. RBAC (içindeki roller ve izinler) bir sonraki aşamada kendi trait’idir. Trait-slot teriminin amacı, her aşamanın bir dilim ekleyerek modeli yeniden yazmadan veya ev sahibinin kendi koduyla çakışmadan eklenmesidir.

Şirket modeli kendisi kasıtlı olarak ince tutulmuştur. Temel yalnızca her çok kiracılı uygulamanın ihtiyaç duyduğu sütunları (isim, slug, uuid, ülke, kurucu) içerir. Ev sahibi bunu genişletir ve bir yapılandırma değeriyle alt sınıfa işaret eder, böylece iş mantığı sütunları eklenebilir. Faturalama, hasAccess() aşama-3 bileşeni: bugüne kadar ücretsiz çekirdek üzerinde true döner ve ücretli faturalama eklentisi daha sonra bunu gerçek abonelik mantığı ile geçersiz kılacaktır, çağrı noktası değiştirmeden.


Davetiyeler ve Kaldırdığım E-posta Sahtekarlığı Koruyucusu

Birini e-posta ile davet edersiniz. Paket, 64 karakterlik rastgele bir belirteci ve sert bir son kullanma tarihi depolar ve bir bağlantı e-postası gönderir. Şu ana kadar, normal.

Önemli koruyucu, kabul aşamasında devreye girer. Tek bir belirteç, bir şirkete katılmak için yeterli değildir. Oturum açmış kullanıcının e-posta adresinin, davet edilen e-posta ile eşleşmesi gerekir:

public function isFor(string $email): bool
{
    return mb_strtolower($this->email) === mb_strtolower($email);
}

Eski uygulamada bu vardı ve onu korudum çünkü bununla, sızan bir davet bağlantısı, onu elinde tutan herkes için şirkete geçiş yapma şansını verir. Bu savunma, yalnızca ona adreslenmiş kişiye karşı faydalıdır.

Bu oldukça güvenli hissediyordu. Değildi. Şimdi de iki açığı paylaşabilirim.


Şimdi İki Açık

İnceleme sürecimi gerçekleştirdim, oldukça çaba gerektiren çok açıdan kod incelemesi. İlk tur: yaklaşık bir düzine bulgu, eyleme geçirilebilir olanları hallettim ve “bu temelde temiz” hissiyatı aldım. Ardından, aynı farklılıklar üzerinde ikinci bir inceleme yaptım. İkinci tur, ilkinin gözden kaçırdığı iki güvenlik bulgusu yüzeye çıkardı. İşte her ikisi de.


Açık 1: Davet Akışı Üzerinden Şirket Ele Geçirme

Yukarıdaki e-posta eşleşme koruyucusu, hesabın e-posta adresinin davet edilen e-posta ile eşleştiğini kontrol eder. Güzel. Ama üç şey bir araya geldi:

  1. Kabul rotası auth arkasındaydı, ancak verified arkasında değildi.
  2. Yerel kayıt, email_verified_at = null ayarladı. Herhangi bir e-posta adresinde kayıt oluşturmak için sahipliğini kanıtlamanıza gerek yoktur.
  3. Açık davet sayfası, davetli e-posta adresini props’larına işliyor.

Bunları bir araya getirin. Bir saldırgan sızan bir davet bağlantısını alır (özellikle sızıntılar meydana gelir, bağlantılar iletilir, günlüklerde kalır). Kamuya açık iniş sayfası, kendilerine gönderildiği e-posta adresini verir. O e-posta adresiyle yeni bir hesap kaydedebilir, kanıt göstermeden. email artık davetli e-posta ile eşit olur, bu yüzden isFor() geçerlidir. Kabul eder. Hiç sahip olmadan bir şirkete girer.

Bu durumda zayıf olan şey, e-posta eşleşmesinin kendisi değildir. Zayıf olan kısım, “bu e-posta var mı” ve “bu e-posta sahibi mi” şeklindeki iki kavramın birbirinin yerine geçmesine izin vermekti ve iniş sayfası e-postayı ücretsiz olarak sundu.

İki düzeltme. Kabul yolu artık doğrulanmış bir e-posta gerektiriyor, böylece adresi almak yeterli değil; posta kutusunun sizin olduğuna dair kanıt göstermelisiniz:

if (! $invitation->isFor($user->email) || ! $this->hasVerifiedEmail($user)) {
    return redirect("https://dev.to/")->with('error', __());
    }

Ve e-posta tamamen kamuya açık sayfanın from props’larından kaldırıldı. İniş sayfası artık “bu şirkete katılmak üzere davet edildiniz” diyor ve hiç kimseye dair bilgi vermiyor. Eğer bağlantıyı size göndermedilerse, sayfa size hiç bir yararlı bilgi vermez.


Açık 2: Sahip Olana Geri Privilege

Bu daha ince ve daha fazla hoşuma gidiyor.

Bir çalışanı çıkarmak, yumuşak silmedir. Pivot satırı denetim için kalır, is_deleted = true ile. Birini yeniden eklemek, yeni birini oluşturmak yerine aynı satırı geri yükler (şirket + kullanıcı üzerinde bir benzersiz kısıtlama vardır). Yeniden ekleme ise sahipliğe bağlıdır: zaten bir sahip iseniz, basit bir üye olarak yeniden eklenmek sessizce sizi aşağıya indirmemelidir. Yani yeni sahip bayrağı, zaten bulunan satırla birlikte OR’de birleştirildi:

'is_owner' => $isOwner || ($existingRow && $existingRow->is_owner),

Makul görünüyor. Şimdi bunu düşünelim.

Biri bir sahip. Onu çıkardığınızda (yumuşak silme, satır is_owner = true tutar çünkü kaldırma yalnızca is_deleted olarak değiştirilmiştir). Aylar sonra onu sadece standart bir üye olarak davet ederseniz. $isOwner false olur. Ama $existingRow->is_owner hala önceden truedir. OR durumu geri yükler. Basit bir üye yeniden davet edildiğinde, sessizce tekrar sahiplik kazanır.

Düzeltme, giderme yolunda bir satır eklemektir: bir kişiyi yumuşak bir şekilde sildiğinizde sahiplik bayrağını temizlemek. Böylece pasif bir satır, OR’nin kritik durumuna sahip olmak için bir eski sahip bayrağını taşımaz.

public function removeEmployee(Authenticatable $user): void
{
    $this->users()->updateExistingPivot($user->getAuthIdentifier(), [
        'is_deleted' => true,
        'is_owner' => false, // bir sahibi kaldırırken geri dönmesini engelle
    ]);
}

Sahiplik özelliği hala orada, ama artık yalnızca şu anda üye olan kişilerle sınırlıdır, bu da yalnızca uygulanması gereken tek yerdir.


Neden İlk Geçişte Yakalanamayan İkinci Geçiş

Sanki ilk geçiş ve ben aynı yöntemle inceleme yapıyorduk: farklılıkları okumak, her değişiklikte doğru olup olmadığını kontrol etmek. Her iki açık da birbirinin izinde doğru. Kabul koruyucusu, e-postaları doğru bir şekilde eşleştirir. Sahip-OR doğru bir şekilde sahipliği korur. Hiçbiri, kendi satırında yanlış değil. Tehlikeli hale gelene kadar durumu akışın tamamında izlemek gerekir: kaydet -> e-posta gör -> kabul et veya çıkar -> bekle -> yeniden davet et.

“8 bulgu halledildi” bana ilerleme sağlayacak bir sayı verdi ve gerçekten iki önemli bulgunun varlığını bildirmedi. İkinci run daha zeki değildi, yalnızca tekrar bu soruyu sordu: “bir kiracı sınırını hangi şey geçiyor?” ve bu soru, “bu satır doğru mu” bulamadı.


Testler

Her parça Pest kapsamına sahip ve yukarıdaki iki deliği içerecek şekilde regresyon testleri artık suite’de var, yani sessizce geri gelemezler. Önem verdiğim birkaç tane:

it('GÜVENLİK: KİTLESİZ e-posta ile onaylı hesaplardan kabulü reddeder', function () {
    [$company,$invitation] = companyWithInvite();
    $unverified = User::create([=> ,
        => null,
    ]);

    $this->actingAs($unverified)
        ->post({$invitation->token}/accept")
        ->assertRedirect();

    expect($unverified->fresh()->companies()->count())->toBe(0);
});

it(, function () {
    // sahibi kaldırın, sonra sade bir üye olarak yeniden ekleyin ve sahip olmadıklarını doğrulayın.
});

it(, function () {
    $this->withHeader(,)
        ->get($invitation->token")
        ->assertOk()
        ->assertJsonMissing([=>]);
});

Fail-closed kapsamının kendi testleri de vardır: aktif kiracı yoksa, sıfır satır döndürür, yabancı şirketin verisi görünmez, oluşturma yüklemesinde sahte bir company_id bulunursa, çözücü tarafından yazılır ve geçersiz sayılmaz.

Paket test suite’i yeşil (Pest ve Vue bileşeni testleri dahil), ve sonra benim süreçte yer alması gereken o şey: v0.3.0 sürümünü gerçek ana uygulamaya entegre ettim ve üzerindeki bağlantıları tıkladım. Bir şirket oluşturma, birini davet etme, aktif şirket geçişi yapma, doğrulanmış ve aktif kiracı ara katmanı altında doğru sayfada olma. Bu entegrasyon testi, şimdi yedi ana test olan başka bir testle ve bir yapılandırma sorunu buldum (eski yayım yapılmış yapılandırma dosyası yeni kiracı anahtarlarının üzerine duruyordu, çünkü Laravel’in yapılandırma birleştirmesi, zaten var olan bir alt anahtar grubuna kaydedilmemiş olan alt anahtarları eklemiyor).


Bu Durum Temel Yapının Neresinde Bırakıyor

Üç sürümden sonra paket artık temel bileşenleri, kimlik doğrulama ve çoklu kiralamayı içermektedir. İşte bu sırt. Bir sonraki aşama RBAC, bir şirketteki roller ve izinlerdir ve bunun için trap zaten eklendi: bir şirket oluşturmak, rolleri aşaması dinleyecek bir etkinlik ateşler, böylece varsayılan rolleri tohumlayabilir. Faturalama, daha sonra hasAccess() ögesi üstünde duracak, bugüne kadar ücretsiz bir çekirdek döküntüsü için true döndürülür.

Bir çok kiracılı Laravel uygulaması inşa ediyorsanız, burada alacağım tek şey, kiracı kapsamınızı kapalı bir şekilde bırakmak ve boş bir kiracının hiçbir şey göstermediğini kanıtlayan testi yazmak. Varsayılan-red versiyonu size yalnızca bir whereRaw('0 = 1') maliyetine ve yıllardır gönderimde hatalı bir sorun almanızı sağlar.

Kod GitHub‘da mevcuttur. Kaynağı erişilebilir, ticari olmayan kullanım için ücretsizdir. İnşaatı takip etmek istiyorsanız, repo ve seri uygun yerdir.

Bir sonraki: RBAC.

Kaynak: Orijinal Makale

Contents
  • Buradaki Çoklu Kiralama Anlamı
  • Önemli Karar: Fail-Closed
  • Çözücü ve Neden Bir Arayüzün Ardında
  • Kullanıcı Tarafı Bir Trait’tir, Temel Sınıf Değil
  • Davetiyeler ve Kaldırdığım E-posta Sahtekarlığı Koruyucusu
  • Şimdi İki Açık
    • Açık 1: Davet Akışı Üzerinden Şirket Ele Geçirme
    • Açık 2: Sahip Olana Geri Privilege
    • Neden İlk Geçişte Yakalanamayan İkinci Geçiş
  • Testler
  • Bu Durum Temel Yapının Neresinde Bırakıyor
Yüksek Trafikli Laravel Uygulamaları için Veritabanı Optimizasyon Teknikleri
Aylık 5 Dolarla Laravel Stacks: Deploynix ile Bütçe Dostu Üretim Yönetimi
Eloquent’in increment() Fonksiyonunun İçinde Bulduklarım
Next.js ve Laravel ile Özel “Shadcn-Stili” Kütüphanesi Kullanarak SEO Uyumlu Blogları Nasıl Otomatikleştirdim
JWT’leri Yerel Depolamada Saklamayı Durdurun: HttpOnly Cookie Mimarisi 🛡️ – DEV Community
Bu Makaleyi Paylaş
Facebook Bağlantıyı Kopyala Yazdır
Paylaş
Önceki Makale Wander ile Küçük Ağı Keşfedin
Sonraki Makale İki yıllık PrivadoVPN aboneliğinde %90 indirim ve 3 ay bedava!

Sanal Medya

FacebookBeğen
452Takip Et
PinterestSabitle
237Takip Et

Son Eklenenler

Anthropic Yeni Modellerine Erişimi Durdururken Hindistan’ın AI Geleceği Tartışılıyor
Genel
Meta’nın Beijing’e tepki olarak 2 milyar dolarlık anlaşmayı geri çektiği iddia edildi
Yapay Zeka
Mumbai’de Web Geliştirme Hizmetleri: HTML, CSS, PHP, Laravel, React ve Next.js
Yazılım
SteelSeries’ten Beklenen Eğlenceli South Park Oyun Aksesuarı
Oyun
Amazon güvenlik araştırması Beyaz Saray’ın Anthropic Fable yasağına neden oldu
Liste
Eski çalışan, eski işvereni hedef alan siber saldırılarla hapse girdi!
Siber Güvenlik
//

Siber güvenlik, yapay zeka ve savunma sanayiinden; finans ve sinema dünyasına uzanan geniş bir yelpaze. Teknomers; teknoloji, strateji ve yazılım dünyasını sade bir dille sizlerle buluşturuyor.

Kurumsal

  • Hakkımızda
  • Gizlilik politikası
  • Tanıtım Yazısı ve Backlink Hizmeti

Kategoriler

  • Teknoloji
  • Oyun
  • Sinema
  • Siber Güvenlik
  • Bilim
  • Finans
  • Dünyadan Güncel Haberler

Populer

  • TV'de Ücretsiz İzlenebilen Şifresiz Erotik Kanallar (2025 Güncel Frekans Listesi)

  • The Last of Us PC Kontrolleri: Hızlı Silah Değiştirme ve Tüm Tuşlar (2025)

  • Hogwarts Legacy'de Odaklanma İksiri Nasıl Yapılır?

Teknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor HaberleriTeknomers | Dünyadan Güncel Teknoloji | Oyun | Müzik | Film | Spor Haberleri
Bizi Takip Et
© 2026 Teknomers. All Rights Reserved.
Welcome Back!

Sign in to your account

Kullanıcı Adı veya E-posta Adresi
Şifre

Şifrenizi mi unuttunuz?