Uygulama Seviyesinde İzolasyon Hatası
Smart Tech Devs’te bir B2B SaaS platformu tasarlarken, veri izolasyonu en önemli önceliğinizdir. Laravel’deki standart yaklaşım, her veritabanı sorgusuna otomatik olarak where('tenant_id', $id) şartını eklemek için global scope kullanmaktır. Eloquent global scope’ları kullanışlı olsa da, uygulama katmanında uygulanırlar. Eğer bir geliştirici kazara withoutGlobalScopes() kullanırsa ya da Eloquent’ı atlayarak ham SQL sorgusu çalıştırılırsa, Tenant A, aniden Tenant B’nin fatura bilgilerini görebilir. Kurumsal yazılımlarda bu, felaket niteliğinde bir veri ihlalidir.
Gerçekten dayanıklı, rahat uyku sağlayan bir mimari inşa etmek için, çok kiracılı izolasyonu en alt katmana indirmeliyiz: veritabanına. Bunu PostgreSQL Row-Level Security (RLS) kullanarak başarırız.
Row-Level Security Nedir?
PostgreSQL RLS, veritabanı seviyesinde görünen bir güvenlik görevlisi gibi davranır. Bir tablo üzerinde etkinleştirildiğinde, hangi satırların görünür veya değiştirilebilir olduğunu belirten katı SQL politikaları tanımlarsınız. Eğer veritabanı motoru, tenant ID’ye dayanarak sorguyu reddederse, uygulama seviyesindeki hatalar veya atlanmış scope’lar veri sızıntısına neden olamaz.
Adım 1: Laravel Migration ile RLS’yi Etkinleştirmek
RLS’yi etkinleştirmek ve katı izolasyon politikamızı tanımlamak için migrations içinde ham SQL ifadelerini kullanırız. invoices tablosunu güvence altına alalım.
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class CreateInvoicesTable extends Migration
{
public function up(): void
{
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained();
$table->decimal('amount', 10, 2);
$table->timestamps();
});
// 1. Tablo üzerinde Row-Level Security'yi etkinleştir
DB::statement('ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;');
// 2. Politikanın tablo sahipleri için de zorlanmasını sağla (güvenlik için kritik)
DB::statement('ALTER TABLE invoices FORCE ROW LEVEL SECURITY;');
// 3. Politika Oluştur: Bir satır yalnızca tenant_id'si
// oturumda ayarlanmış 'app.current_tenant_id' değişkeniyle eşleştiğinde görünür.
DB::statement("
CREATE POLICY tenant_isolation_policy ON invoices
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint);
");
}
public function down(): void
{
DB::statement('DROP POLICY IF EXISTS tenant_isolation_policy ON invoices;');
Schema::dropIfExists('invoices');
}
}
Adım 2: Veritabanı Bağlamını Middleware ile Ayarlamak
Veritabanı politikasının sorguyu değerlendirmek için app.current_tenant_id‘ye ihtiyaç duyması nedeniyle, bu değişkeni her HTTP isteğinin başlangıcında PostgreSQL oturumuna enjekte etmemiz gerekir. Bunu Laravel Middleware ile temiz bir şekilde yapıyoruz.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SetPostgresTenantContext
{
public function handle(Request $request, Closure $next)
{
// Tenant'ı alan veya kimlik doğrulama kullanıcısı aracılığıyla çözümle
$tenantId = $request->user()?->tenant_id;
if ($tenantId) {
// Tenant ID'sini doğrudan PostgreSQL oturumuna enjekte et
DB::statement("SET app.current_tenant_id = '{$tenantId}'");
} else {
// Kamuya açık rotalar için, bunu tamamen geçersiz kıl
DB::statement("SET app.current_tenant_id = ''");
}
return $next($request);
}
}
Mühendislik ROI’si
Tenant kapsamınızı PHP’den PostgreSQL’e geçirdiğinizde, veri sızıntısı açısından mimari olarak dayanıklılık sağlarsınız. Hatta eğer bir junior geliştirici DB::select('SELECT * FROM invoices') yazsa bile, PostgreSQL isteği yakalar, oturumun app.current_tenant_id‘sini kontrol eder ve yalnızca bu belirli tenant’ın kayıtlarını döner. Bu, yüksek riskli B2B SaaS ortamları için en büyük koruma mekanizmasıdır.
Kaynak: Orijinal Makale


