Skip to main content
Sistem soft multi-tenancy kullanır: tüm tenant’lar tek veritabanını paylaşır, izolasyon kayıt seviyesinde organization_id ile sağlanır. DB-per-tenant veya schema-per-tenant yoktur.

Tenant kimliği nereden gelir

Tenant, JWT claim’inden okunur. İki eşdeğer claim kabul edilir:
  • tenant_id (birincil)
  • organization_id (Keycloak Protocol Mapper uyumluluğu; dev provisioning bunu kurar)
HttpContextCurrentTenant (SeedWork/Security/HttpContextCurrentTenant.cs) fallback uygular:
public Guid? TenantId
{
    get
    {
        var value =
            User?.Claims.FirstOrDefault(c => c.Type == ...TenantId)?.Value          // önce tenant_id
         ?? User?.Claims.FirstOrDefault(c => c.Type == ...OrganizationId)?.Value;   // sonra organization_id
        return value != null && Guid.TryParse(value, out var id) ? id : null;
    }
}
public bool HasTenant => TenantId.HasValue;
ICurrentTenant Program.cs → RBAC bölümünde kaydedilir:
builder.Services.AddScoped<ICurrentTenant, HttpContextCurrentTenant>();

Domain arayüzleri

Tenant’a ait varlıklar SharedKernel’deki marker arayüzlerini implement eder:
// Tenant'a ait OLABİLEN (nullable — global kayıtlar da olabilir)
public interface IMayHaveTenant
{
    Guid? OrganizationId { get; }
}

// Her zaman bir tenant'a ait
public interface ITenant
{
    Guid OrganizationId { get; }
}
IMayHaveTenant global (tenant-agnostic) kayıtlara da izin verir; ITenant ise zorunlu tenant bağı ifade eder.

Pipeline-level izolasyon

Command/query bir tenant kaynağına eriştiğinde IRequireTenantAccess implement eder; bunu AuthorizationPipelineBehavior (Application/SeedWork/PipelineBehaviors/) handler çalışmadan önce doğrular:
public interface IRequireTenantAccess
{
    Guid TenantId { get; }   // route/body'den gelen organizasyon ID
}
if (request is IRequireTenantAccess tenantRequest)
{
    if (!_currentUser.IsInTenant(tenantRequest.TenantId))
        throw new ForbiddenException("Farklı bir organizasyona ait kaynağa erişim engellendi.");
}
Yani kullanıcı kendi token’ındaki tenant dışında bir TenantId ile istek atarsa 403 alır. Bu kontrol controller attribute’undan bağımsızdır; use-case seviyesinde garanti verir.

Neden DB-per-tenant değil

Soft-tenant tercih edildi çünkü:
  • Tek migration, tek bağlantı havuzu — N tenant için N veritabanı yönetmek operasyonel yük.
  • Müşteri on-prem ortamı — mevcut tek Postgres instance’ı paylaşılır; dinamik veritabanı oluşturma yetkisi her zaman olmayabilir.
  • Cross-tenant raporlama tek sorguda mümkün (yetki dahilinde).
Karşılığında izolasyon disiplini uygulamaya düşer: tenant’a ait sorgular organization_id filtresini taşımalı ve mutasyonlar IRequireTenantAccess ile korunmalıdır.
Soft-tenant’ta izolasyon “varsayılan olarak açık” değildir — bir sorguda tenant filtresini unutmak veri sızıntısıdır. Tenant’a ait yeni bir aggregate eklerken IMayHaveTenant/ITenant implement etmeyi ve ilgili command/query’lerde IRequireTenantAccess kullanmayı ihmal etmeyin.

İlgili

Authorization

Permission ve pipeline behavior.

Realm Yapısı

organization_id Protocol Mapper.

Tasarım Kararları

Soft-tenant tercihi ve gerekçeleri.

DbContext

Query filter ve şema yapısı.