TL;DR
TL;DR
- Çok kiracılı (multi-tenant) bir uygulama, etkin kiracıyı request (alt domain/başlık) üzerinden çözmekteydi, bunun yerine authenticated user üzerinden çözmesi gerekiyordu.
- Bu durum, “ben hangi kiracıdayım” sorusunun doğruluğunun istemciye bağlı olmasına neden oluyordu; bu yanlış bir yaklaşım.
- Düzeltme: Kiracıyı, kullanıcının organizasyon üyeliğinden türetmek, bunu middleware içinde zorunlu kılmak ve kapalı başarısızlığı sağlamak. Bir test bu davranışı kilitler.
Hata, bir cümlede
Hata, bir cümlede
Request, uygulamaya hangi kiracıyı yüklemesi gerektiğini söylüyordu ve uygulama buna inanıyordu.
Bir çok kiracılı SaaS uygulamasında, her sorgu örtük olarak kapsamlıdır: “bana bu kiracının panolarını ver.” Eğer kiracı kimliği, istemcinin kontrol ettiği bir şeyden geliyorsa — bir alt domain, başlık, rota parametresi — o zaman kapsam yalnızca istemcinin güvenilirliği kadar güvenlidir. Bu, bir açık kapı durumuna yol açar.
Güvenin nerede olmalı
Güvenin nerede olmalı
Bunu bir bina kartı gibi düşünün. Request, birinin “ben buradayım, 9. kat için” demesi gibidir. Üyelik kaydı, hangi katlara erişim izniniz olduğunu belirten karttır. Siz kartı kontrol edersiniz, iddiayı değil.
| Önce | Sonra | |
|---|---|---|
| Doğruluk kaynağı | request (alt domain / başlık) | kullanıcının organizasyon üyeliği |
| Kiracı kim tarafından belirlenir | istemci | sunucu |
| Başarısızlık durumu | kullanıcı, ait olmadığı bir kiracıya geçiş yapabilir | çözüm kapalı başarısızlık ile sonuçlanır |
| Test edilebilir mi? | zor — request şekline bağlı | evet — kullanıcıya bağlı |
Düzeltmenin şekli
Düzeltmenin şekli
Kiracıyı, authenticated kullanıcının organizasyonundan çözmek için bir middleware oluşturun; tenant-scoped işlemler başlamadan önce bunu yapın:
final class SetTenantContext
{
public function handle(Request $request, Closure $next): Response
{
$org = $request->user()?->currentOrganization();
// Organizasyon yok, tenant context yok. Kapalı başarısızlığı sağlıyoruz, tahmin etmiyoruz.
abort_if($org === null, 403, 'No organization context.');
Tenancy::setCurrent($org->tenant); // sunucudan elde edilen, request'ten değil
return $next($request);
}
}
Önemli olan kısım setCurrent() değil — değerin $request->user() üzerinden elde edilmesidir, $request üzerinden değil. Kullanıcı kimliği doğrulanmıştır; alt domain değil.
request ──> [auth] ──> [SetTenantContext] ──> tenant-scoped routes
│
└── tenant = user's org (NOT the URL/header)
Bunu bir test ile kilitleyin
Bunu bir test ile kilitleyin
Böyle bir sızıntı, sessizce geri dönebilir. Bu nedenle, düzeltme, biri bunu tekrar ortaya çıkarırsa bir testin ses çıkarabilmesi ile tamamlanmalıdır:
it('kullanıcının ait olmadığı bir kiracıyı asla çözmemelidir', function () {
$userA = User::factory()->inOrganization($orgA = Organization::factory()->create())->create();
$orgB = Organization::factory()->create();
// İstemci "org B" talep etse bile, kullanıcı A her zaman org A'da kalmalıdır.
actingAs($userA)
->withHeader('X-Tenant', $orgB->tenant->getKey())
->get()
->assertOk();
expect(Tenancy::current()->is($orgA->tenant))->toBeTrue();
});
Sonuç
Sonuç
Bir değer veriyi kapsıyorsa, bu, istemcinin oluşturamayacağı bir kaynaktan gelmelidir. Kiracı çözümlemesi için bu, authenticated kullanıcının üyeliğini gerektirir — bu server tarafında doğrulanmalı ve eksik olduğunda kapalı başarısızlıkla sonuçlanmalıdır. Kimliğinizi, kim olduğunuzdan ziyade, ne talep ettiğinize göre çözün.
Kaynak: Orijinal Makale


