2026-03-15
Multi-Tenancy, Kütüphaneler ve Mimari Kararlar
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı bir kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini seçmek için karar çerçevelerini kullanın.
Abstract
Post 101 bir izin sistemi için yedi hedef belirledi ve dağınık kontrol anti-pattern’ini ortaya koydu. Post 102 yetkilendirmeyi bir service layer içinde merkezileştirdi. Post 103 type-safe RBAC ekledi. Post 104 rol-izin matrisini ABAC policy engine ile değiştirdi. Post 105 ABAC’ı çevre koşulları, alan düzeyinde okuma/yazma izinleri ve veritabanı sorgu filtreleme ile genişletti.
Üç üretim sorunu kaldı. Birincisi, sistemin tenant sınırı yok — Organizasyon A’daki bir kullanıcı, doğru sorgu ile Organizasyon B’nin kaynaklarına erişebilir. İkincisi, özel ABAC motoru çalışıyor ama şu soru ortaya çıkıyor: ekip özel yetkilendirme kodunu mu sürdürmeli yoksa CASL gibi bir kütüphaneye mi geçmeli? Üçüncüsü, RBAC, özel ABAC, kütüphane tabanlı ABAC ve harici policy engine’ler arasında seçim yapmak için kapsamlı bir karar çerçevesi yok.
Bu kapanış yazısı üç boşluğu kapatıyor: birinci sınıf bir izin kavramı olarak multi-tenancy, dürüst sürtünme analizi ile çalışan bir CASL geçişi ve serinin tüm yaklaşımları kapsayan kesin karşılaştırması.
Multi-Tenancy Modelleri
Tenant Kavramı
SaaS’ta bir tenant, kullanıcıları ve kaynaklarını gruplayan bir organizasyon, çalışma alanı veya hesaptır. Slack çalışma alanları, GitHub organizasyonları ve Notion çalışma alanları birer tenant’tır. Tenant sınırı, en dıştaki izin sınırıdır — rolleri, sahipliği veya alan erişimini kontrol etmeden önce, sistem kullanıcının kaynağın sahibi olan tenant’a ait olduğunu doğrulamalıdır.
Domain Modelini Genişletme
Serinin domain modeli bir tenant boyutu kazanıyor:
interface User {
userId: string;
role: Role;
departmentId?: string;
tenantId: string; // kullanıcının ait olduğu organizasyon
tenantRole?: TenantRole; // tenant içindeki rol (owner, admin, member)
}
interface Document {
id: string;
title: string;
content: string;
authorId: string;
status: 'draft' | 'published' | 'archived';
projectId: string;
departmentId: string;
tenantId: string; // bu dokümanın sahibi olan tenant
}
interface Project {
id: string;
name: string;
ownerId: string;
departmentId: string;
tenantId: string; // bu projenin sahibi olan tenant
}
type TenantRole = 'owner' | 'admin' | 'member';
Her kaynak artık bir tenantId taşıyor. Her kullanıcı tam olarak bir tenant’a ait (basitlik için — çoklu tenant üyeliği mümkün ama bu kapsamın dışında karmaşıklık ekliyor).
Üç İzolasyon Stratejisi
Satır Düzeyi İzolasyon (paylaşılan şema): Tüm tenant’lar aynı tabloları paylaşır. Her tabloda bir tenant_id sütunu bulunur. En basit altyapı ve en ucuz seçenek, ancak bir WHERE tenant_id = ? koşulunu unutmak çapraz tenant veri sızıntısına neden olur. PostgreSQL Row-Level Security (RLS), veritabanı düzeyinde bir güvenlik ağı olarak bunu zorunlu kılabilir.
Şema Düzeyi İzolasyon: Her tenant aynı veritabanı içinde ayrı bir şema alır. Daha güçlü izolasyon — eksik WHERE koşulu veri sızıntısı yerine hata üretir. Migration’lar N şema üzerinde çalıştırılmalıdır. Onlarca ila yüzlerce tenant için uygundur.
Veritabanı Düzeyi İzolasyon: Her tenant özel bir veritabanı örneği alır. Maksimum izolasyon ve en güçlü uyumluluk duruşu. En yüksek maliyet ve operasyonel karmaşıklık.
| Boyut | Satır Düzeyi | Şema Düzeyi | Veritabanı Düzeyi |
|---|---|---|---|
| Altyapı maliyeti | Düşük | Orta | Yüksek |
| İzolasyon gücü | Uygulama düzeyinde | DB-şema düzeyinde | Fiziksel izolasyon |
| Tenant sayısı ölçeklenebilirliği | Binlerce+ | Yüzlerce | Onlarca |
| Migration karmaşıklığı | Tek migration | N migration | N migration + N veritabanı |
| Çapraz tenant sorgu riski | Yüksek (eksik WHERE) | Düşük (yanlış şema = hata) | Yok |
| Tenant başına özelleştirme | Sınırlı | Orta | Tam |
| Uyumluluk uygunluğu | Standart | SOC 2 / ISO | HIPAA / PCI-DSS |
Bu seri satır düzeyi izolasyon modelini kullanıyor çünkü en yaygın başlangıç noktası ve izin perspektifinden en zorlu olanı. Şema ve veritabanı izolasyonu tenant sınırlarını altyapı düzeyinde çözer. Satır düzeyi izolasyon, bu sınırları uygulamanın zorunlu kılmasını gerektirir.
Tenant-Aware İzin Katmanı
Dağınık Tenant Kontrolü Anti-Pattern’i
Tenant-aware izinler olmadan, her service metodu tenant izolasyonunu manuel olarak kontrol eder:
// Anti-pattern: her service metodunda manuel tenant kontrolü
async function getDocumentById(documentId: string) {
const session = await requireSession();
const document = await db.document.findUnique({ where: { id: documentId } });
// Manuel tenant kontrolü -- unutulması kolay
if (document.tenantId !== session.tenantId) {
throw new ForbiddenError();
}
// Sonra normal ABAC kontrolü
if (!can(session, 'read', 'document', document)) {
throw new ForbiddenError();
}
return filterFields(session, 'document', document);
}
Bu, Post 101’deki dağınık kontrol pattern’inin tenant düzeyinde yeniden ortaya çıkmasıdır. Bir geliştirici tek bir endpoint’te tenant kontrolünü unutursa, çapraz tenant veri sızıntısı oluşturur — diğer müşterilerin verilerini ifşa ettiği için en tehlikeli yetkilendirme hatası sınıfıdır.
Tenant İzolasyonunu Global ABAC Koşulu Olarak Tanımlama
Doğru yaklaşım: tenant izolasyonu, her izin kontrolünde otomatik olarak çalışan yerleşik bir koşul haline gelir:
// Tenant izolasyonunu yerleşik global koşul olarak tanımlama
const permissions = new PermissionBuilder()
// Global koşul: TÜM roller, TÜM kaynaklar için geçerli
.global((user, data) => {
// Her kaynağın tenantId'si kullanıcının tenantId'si ile eşleşmeli
if ('tenantId' in data && user.tenantId !== data.tenantId) {
return false; // Çapraz tenant erişimi: REDDET
}
return true; // Aynı tenant: role-specific kontrollere devam et
})
.role('admin')
.can('manage', 'document')
.can('manage', 'project')
.role('editor')
.can(['read', 'update'], 'document', [
(user, doc) => user.departmentId === doc.departmentId,
])
// ... Post 104-105'ten kalan politikalar
.build();
global() koşulu herhangi bir role-specific koşuldan önce çalışır. Her izin kontrolünde örtük bir WHERE koşulu gibi davranır. Bir geliştirici yeni bir rol veya yeni bir kaynak türü oluştursa bile, tenant izolasyonu otomatik olarak zorunlu kılınır.
can() Fonksiyonunu Güncelleme
can() fonksiyonu önce global koşulları değerlendirir:
function can<R extends Resource>(
user: User,
action: Action,
resource: R,
data?: ResourceDataMap[R],
env?: Environment
): boolean {
// Adım 1: Global koşulları değerlendir (tenant izolasyonu)
if (data) {
for (const globalCondition of permissions.globalConditions) {
if (!globalCondition(user, data as Record<string, unknown>)) {
return false; // Global koşul başarısız (ör. yanlış tenant)
}
}
}
// Adım 2: Rol + kaynak + aksiyon için eşleşen girdileri bul
// (Post 104-105 ile aynı mantık)
const entries = permissions[user.role] as PermissionEntry<R>[];
for (const entry of entries) {
if (entry.resource !== resource) continue;
if (!entry.actions.includes(action)) continue;
if (!entry.conditions || entry.conditions.length === 0) return true;
if (data) {
const allMet = entry.conditions.every(c => c.evaluate(user, data, env));
if (allMet) return true;
}
}
return false; // Varsayılan olarak reddet
}
Veritabanı Sorgu Filtrelemesini Güncelleme
Post 105’teki toWhereClause() fonksiyonu tenant filtreleme içermelidir:
function toWhereClause<R extends Resource>(
user: User,
resource: R,
action: Action,
env?: Environment
): WhereClause<R> | null {
// Her zaman tenant filtresini ekle
const tenantFilter = { tenantId: user.tenantId };
const roleFilter = buildRoleFilter(user, resource, action, env);
if (roleFilter === null) return null; // Erişim yok
// Tenant filtresini role-specific filtre ile birleştir
return { ...tenantFilter, ...roleFilter };
}
Tenant filtresi her zaman mevcuttur. buildRoleFilter() {} döndürse bile (admin için ek filtre yok), sorgu yine de WHERE tenantId = ? içerir.
Çapraz Tenant Erişimi: İstisna
Bazı senaryolar çapraz tenant erişimi gerektirir:
- Platform yöneticileri (süper adminler) tüm tenant’ları yöneten
- Paylaşılan kaynaklar (şablonlar, herkese açık içerik) herhangi bir tenant’ın dışında var olan
- Destek araçları müşteri hizmetlerinin tenant verilerini görüntülemesi için
// Platform yöneticisi tenant izolasyonunu atlar
.role('platform_admin')
.global(() => true) // Global tenant kontrolünü geçersiz kıl
.can('manage', 'document')
.can('manage', 'project')
.can('manage', 'tenant')
// Paylaşılan kaynaklarda tenantId yok
interface SharedTemplate {
id: string;
title: string;
// tenantId yok -- tüm tenant'lar erişebilir
}
Warning: Çapraz tenant istisnaları açık ve denetlenebilir olmalıdır. Platform yöneticisi rolü, tenant düzeyindeki yöneticiden ayrı olmalı ve Post 105’teki çevre koşulları ile zorunlu kılınan ek kimlik doğrulama gereksinimleri (MFA, IP kısıtlamaları) içermelidir.
Neden Bir İzin Kütüphanesi Kullanmalı?
Build vs. Kütüphane Kararı
Post 101-105, RBAC, ABAC, alan düzeyinde izinler, DB sorgu filtreleme, çevre kuralları ve şimdi multi-tenancy’yi kapsayan özel bir izin sistemi oluşturdu. Bu yaklaşık 300-500 satır çekirdek izin mantığıdır. Bu kodu sürdürmek ne noktada bir kütüphane benimsemekten daha pahalı hale gelir?
Özel Uygulama Güçlü Yönleri
- Sıfır bağımlılık: Kritik güvenlik yolunda üçüncü taraf kodu yok
- API yüzeyinde tam kontrol:
can()imzası tam olarak gerektiği gibi gelişir - Mükemmel TypeScript entegrasyonu: Generic constraint’ler, builder pattern’ler ve spesifik domain için tasarlanmış type inference
- Serileştirme yükü yok: Düz fonksiyonlar, class instance’ları yok, RSC uyumlu
- Ekip anlayışı: Her koşul ekibin yazdığı bir fonksiyon — kara kutu yok
- Öngörülebilir davranış: Hata ayıklama standart fonksiyon çağrı yığınlarını takip eder
Özel Uygulama Zayıf Yönleri
- Bakım yükü: Ekip hataları, uç durumları ve güvenlik yamalarını sahiplenir
- Sınırlı topluluk testi: Olağandışı uç durumlar üretime kadar ortaya çıkmayabilir
- Özellik yeniden uygulaması: Alan izinleri, DB sorgu dönüşümü, koşul operatörleri (
$in,$ne,$gte) — kütüphanelerin zaten sağladığını yeniden oluşturma - Onboarding maliyeti: Yeni ekip üyeleri belgelenmiş bir kütüphane yerine özel bir API öğrenir
Kütüphane Güçlü Yönleri
- Topluluk tarafından test edilmiş: Binlerce proje, keşfedilen ve düzeltilen uç durumlar
- Yerleşik özellikler: Alan izinleri, MongoDB tarzı koşullar, Prisma/Mongoose adaptörleri
- Dokümantasyon ve topluluk: Öğreticiler, Stack Overflow yanıtları, konferans sunumları
- Azaltılmış bakım: Güvenlik yamaları ve özellik eklemeleri bakımcılar tarafından yönetilir
Kütüphane Zayıf Yönleri
- API kısıtlamaları: Kütüphanenin API’si serinin
can()imzasıyla eşleşmeyebilir - Bağımlılık riski: Kütüphane bakımı yavaşlayabilir veya durabilir
- Entegrasyon sürtünmesi: Class tabanlı kütüphaneler React Server Components ile çakışır
- Kara kutu davranışı: İzin reddini debug etmek kütüphane iç yapısını anlamayı gerektirir
Build vs. Kütüphane Karar Çerçevesi
In-Code vs. DSL Tabanlı Yaklaşımlar
| Yaklaşım | Örnekler | Güçlü Yönler | Zayıf Yönler |
|---|---|---|---|
| In-code (TypeScript) | Özel, CASL | Type-safe, runtime yükü yok, tanıdık dil | Deployment’a bağlı, runtime değişikliği yok |
| DSL / Policy dili | OPA/Rego, Cedar, Cerbos (YAML) | Uygulama kodundan ayrık, geliştirici olmayanlar düzenleyebilir, denetlenebilir | Öğrenme eğrisi, araç yükü, gecikme |
| Hibrit | Permit.io, özel DB’de saklanan kurallar | Runtime yapılandırılabilir + kod tabanlı varsayılanlar | Karmaşıklık, tutarlılık zorlukları |
CASL Entegrasyonu
Bu Seri İçin Neden CASL
CASL, en popüler JavaScript/TypeScript yetkilendirme kütüphanesidir (~6KB çekirdek). İzomorfiktir (sunucu ve istemcide çalışır), ABAC koşullarını, alan düzeyinde izinleri ve veritabanı sorgu dönüşümünü destekler. Seri zaten CASL’ın sağladığı her şeyi oluşturduğundan, doğrudan özellik bazında karşılaştırma mümkündür.
npm install @casl/ability @casl/prisma
Migration: AbilityBuilder
Post 104’teki özel PermissionBuilder, CASL’ın AbilityBuilder’ına eşlenir:
Özel (Post 104-105):
const permissions = new PermissionBuilder()
.role('admin')
.can('manage', 'document')
.role('editor')
.can(['read', 'update'], 'document', [
(user, doc) => user.departmentId === doc.departmentId,
])
.role('author')
.can(['read', 'update'], 'document', [
(user, doc) => doc.authorId === user.userId,
])
.build();
CASL karşılığı:
import { AbilityBuilder, createMongoAbility, MongoAbility } from '@casl/ability';
type Actions = 'create' | 'read' | 'update' | 'delete' | 'manage';
type Subjects = 'Document' | 'Project' | 'all';
type AppAbility = MongoAbility<[Actions, Subjects]>;
function defineAbilitiesFor(user: User): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
createMongoAbility
);
if (user.role === 'admin') {
can('manage', 'all');
}
if (user.role === 'editor') {
can(['read', 'update'], 'Document', { departmentId: user.departmentId });
}
if (user.role === 'author') {
can(['read', 'update'], 'Document', { authorId: user.userId });
can('create', 'Document');
}
if (user.role === 'viewer') {
can('read', 'Document', { status: 'published' });
}
return build();
}
Temel API farklılıkları:
- Koşullar fonksiyonlar yerine MongoDB tarzı nesnelerdir (
{ authorId: user.userId }) - Roller için builder-pattern zincirleme yok — kullanıcı rolünde
if/elsedallanma kullanılır - Negatif kurallar için
cannot()(CASL’a özel — özel sistemde bu yoktu) 'manage'tüm CRUD aksiyonları için CASL’ın joker karakteri;'all'tüm subject’ler için
subject() Helper’ı ve Sürtünmesi
CASL, kontrol edilen nesnenin türünü bilmelidir. Class’larla bu otomatiktir (class adı üzerinden). TypeScript uygulamalarının genellikle kullandığı düz nesnelerde ise subject() helper’ı gereklidir:
import { subject } from '@casl/ability';
// CASL düz nesnelerin sarmalanmasını gerektirir
ability.can('update', subject('Document', document));
// Sorun: subject() nesneyi __caslSubjectType__ ekleyerek mutasyona uğratır
// Bu React Server Components ile çakışır (nesneler serileştirilebilir olmalı)
Geçici çözüm 1: Object spreading
// Orijinali mutasyona uğratmamak için kopya oluştur
ability.can('update', subject('Document', { ...document }));
Geçici çözüm 2: Özel detectSubjectType
import { createMongoAbility } from '@casl/ability';
const ability = createMongoAbility(rules, {
detectSubjectType: (object) => {
// Class adı yerine özel bir özellik kullan
return object.__type || object.constructor?.modelName || 'unknown';
},
});
// Service layer'da döndürülen nesnelere __type ekle
function toDocumentDTO(doc: Document): DocumentDTO & { __type: 'Document' } {
return { ...doc, __type: 'Document' };
}
Geçici çözüm 3: Lambda matcher ile PureAbility (RSC uyumlu)
import {
PureAbility,
AbilityBuilder,
type AbilityTuple,
type MatchConditions,
} from '@casl/ability';
type AppAbility = PureAbility<AbilityTuple, MatchConditions>;
const lambdaMatcher = (matchConditions: MatchConditions) => matchConditions;
function defineAbilityFor(user: User): AppAbility {
const { can, build } = new AbilityBuilder<AppAbility>(PureAbility);
// MongoDB tarzı yerine lambda koşulları -- class'lar olmadan çalışır
can('read', 'Document', ({ authorId }) => authorId === user.userId);
return build({ conditionsMatcher: lambdaMatcher });
}
Tip:
PureAbility+ lambda matcher yaklaşımı en RSC-uyumlu seçenektir, ancak CASL’ın MongoDB tarzı sorgu operatörlerini ve Prisma entegrasyonunu kaybeder. CASL’ın tam özellik seti ile modern React uyumluluğu arasında gerçek bir ödünleşim vardır.
CASL’da Tenant İzolasyonu
function defineAbilitiesFor(user: User): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
createMongoAbility
);
// Tenant izolasyonu: tenantId HER kurala eklenmeli
// CASL'da global() koşulu yok
if (user.role === 'admin') {
can('manage', 'Document', { tenantId: user.tenantId });
can('manage', 'Project', { tenantId: user.tenantId });
}
if (user.role === 'editor') {
can(['read', 'update'], 'Document', {
tenantId: user.tenantId,
departmentId: user.departmentId,
});
}
// Platform yöneticisi: tenantId filtresi yok
if (user.role === 'platform_admin') {
can('manage', 'all');
}
return build();
}
Özel sistemde her kurala otomatik olarak uygulanan bir global() koşulu vardı. CASL, tenantId’yi her kurala ayrı ayrı eklemeyi gerektirir. Bir kuralda eksik bırakmak çapraz tenant sızıntısı oluşturur. Bu önemli bir ergonomik farktır.
CASL Alan ve Veritabanı Entegrasyonu
permittedFieldsOf ile Alan Düzeyinde İzinler
import { permittedFieldsOf } from '@casl/ability/extra';
// Alan düzeyinde kurallar tanımla
can('read', 'Document', ['title', 'content', 'status'], {
status: 'published',
});
can(
'read',
'Document',
['title', 'content', 'status', 'internalNotes', 'reviewComments'],
{ authorId: user.userId }
);
// Belirli bir doküman için izin verilen alanları al
const fields = permittedFieldsOf(ability, 'read', 'Document', {
fieldsFrom: (rule) =>
rule.fields || [
'title',
'content',
'status',
'authorId',
'internalNotes',
'reviewComments',
'publishedAt',
],
});
Post 105’teki özel getVisibleFields() ile karşılaştırın — kavram aynı, API farklı. CASL, bir kuralda alan kısıtlaması olmadığında tüm olası alanları döndüren bir fieldsFrom callback’i gerektirir.
CASL AST’den Prisma Sorgu Dönüşümü
import { accessibleBy } from '@casl/prisma';
// CASL kurallarını Prisma where koşuluna dönüştür
const documents = await prisma.document.findMany({
where: accessibleBy(ability).Document,
});
// İş mantığı filtreleriyle birleştir
const documents = await prisma.document.findMany({
where: {
AND: [accessibleBy(ability).Document, { projectId: projectId }],
},
});
Post 105’teki özel toWhereClause() ile karşılaştırma:
- CASL’ın
accessibleBy()fonksiyonu MongoDB tarzı koşulları Prismawheresözdizimine dönüştürür - Özel
toWhereClause(),toFiltercallback’leri olan koşul tanımlayıcılarını kullanır - CASL, birden fazla eşleşen kural arasında
ORmantığını otomatik olarak yönetir - CASL, hiçbir kural eşleşmezse
ForbiddenErrorfırlatır (fail-closed)
Warning:
accessibleBy()yalnızca MongoDB tarzı koşullarla (createMongoAbility’den) çalışır, lambda koşullarıyla (PureAbility) çalışmaz. RSC uyumluPureAbilitypattern’ini kullanıyorsanız, Prisma sorgu dönüşümünü kaybedersiniz. Bu, mevcut CASL mimarisinde kesin bir ödünleşimdir.
Kapsamlı Karşılaştırma
RBAC vs. Özel ABAC vs. CASL ABAC
| Boyut | RBAC (Post 103) | Özel ABAC (Post 104-105) | CASL ABAC (Bu Yazı) |
|---|---|---|---|
| Çekirdek mantık | Rol-izin araması | Koşullu policy engine | MongoDB koşullu AbilityBuilder |
| Auth kod satır sayısı | ~80 (matris + can()) | ~300-500 (builder + engine + alan + DB) | ~50 (defineAbilitiesFor) + kütüphane |
can() imzası | can(role, resource, action) | can(user, action, resource, data?, env?) | ability.can(action, subject(type, data)) |
| Bağlamsal koşullar | Hayır (helper gerektirir) | Evet (policy builder’da inline) | Evet (MongoDB tarzı nesneler veya lambda’lar) |
| Alan düzeyinde izinler | Hayır | Evet (getVisibleFields, pickPermittedFields) | Evet (permittedFieldsOf) |
| DB sorgu filtreleme | Hayır | Evet (toWhereClause()) | Evet (accessibleBy() ile Prisma) |
| Çevre kuralları | Hayır | Evet (zaman, IP, flag’ler) | Kısmi (özel koşullar ile) |
| Multi-tenancy | Metod başına manuel kontrol | Global koşul (otomatik) | Kural başına tenantId (kural başına manuel) |
| Type safety | Tam (generic’ler, mapped type’lar) | Tam (resource-action generic’ler) | İyi (typed action/subject, koşullarda daha zayıf) |
| RSC uyumluluğu | Tam (düz fonksiyonlar) | Tam (düz fonksiyonlar) | Kısmi (subject() mutasyon sorunu) |
| Negatif kurallar | Hayır | Hayır | Evet (cannot()) |
| Bakım | Ekip sahipliğinde | Ekip sahipliğinde | Kütüphane bakımlı çekirdek |
| Bundle boyutu | 0 (yerleşik) | 0 (yerleşik) | ~6KB (çekirdek) + adaptörler |
Karar Çerçevesi: Hangi Sistemi Seçmelisiniz?
Her Birini Ne Zaman Seçmeli
RBAC (Post 103) — Varsayılan Seçim
- Ekip: Her boyutta
- Uygulama: Dahili araçlar, basit SaaS, net rollere sahip içerik platformları
- Karmaşıklık: Düşük — 2-4 rol, izinler yalnızca role bağlı
- Seçim sinyali: İzin gereksinimleri “bu rol bu şeyleri yapabilir” şeklinde temiz bir eşleme oluşturur
- Yükseltme sinyali:
can()yanında helper fonksiyonlar çoğalmaya başlar
Özel ABAC (Post 104-105) — Tam Kontrol
- Ekip: Yetkilendirme uzmanlığı var, auth kodunu sürdürmeye istekli
- Uygulama: Karmaşık iş kurallarına sahip SaaS, alan düzeyinde görünürlük, büyük veri kümeleri
- Karmaşıklık: Yüksek — sahiplik, departman, durum, zaman koşulları
- Seçim sinyali:
can()kaynak başına 3+ bağlamsal koşulu değerlendirmeli - Yükseltme sinyali: Auth bakımı için ekip bant genişliği azalır; birden fazla ORM’de DB sorgu adaptörleri gerekir
CASL ABAC (Bu Yazı) — Topluluk Tarafından Test Edilmiş Kütüphane
- Ekip: Auth iç yapısı yerine iş mantığına odaklanmak istiyor
- Uygulama: Prisma/MongoDB kullanan SaaS, alan izinleri ve DB filtreleme gerektiren
- Karmaşıklık: Yüksek — ama ekip özel kod yerine kütüphane API’sini tercih ediyor
- Seçim sinyali: Özel ABAC özellik seti CASL’ın yetenekleriyle eşleşiyor
- Kaçınma sinyali: Düz nesnelerle yoğun RSC kullanımı; çevre koşulları gereksinimi; global tenant izolasyonu gereksinimi
Harici PDP (Cerbos, OPA, Cedar) — Servis Olarak Yetkilendirme
- Ekip: Adanmış platform/güvenlik ekibi
- Uygulama: Mikroservisler, polyglot stack, servisler arası paylaşılan yetkilendirme kararları
- Karmaşıklık: Çok yüksek — birden fazla servis tutarlı yetkilendirme gerektirir
- Seçim sinyali: Birden fazla backend aynı yetkilendirme kararlarına ihtiyaç duyar; uyumluluk ayrıştırılmış, denetlenebilir politika yönetimi gerektirir
Yaygın Tuzaklar
-
Bir CASL kuralında tenant izolasyonunu unutma: CASL’da global koşul yoktur. Bir kuralda
tenantIdeksik bırakmak çapraz tenant sızıntısı oluşturur. Her platform-admin olmayan kuralıntenantIdiçerdiğini doğrulayan bir lint kuralı veya birim testi yazın. -
CASL’ın RSC ile sorunsuz çalıştığını varsaymak:
subject()helper’ı nesneleri mutasyona uğratır. React Server Components serileştirilebilir veri gerektirir. CASL Entegrasyonu bölümündeki üç geçici çözümden birini kullanın. -
PureAbility Prisma entegrasyonunu kaybeder: RSC uyumlu
PureAbility+ lambda matcher pattern’i koşulları Prismawherekoşullarına dönüştüremez. Ekipler RSC uyumluluğu ile DB sorgu filtreleme arasında seçim yapmalıdır. -
Erken aşırı mühendislik: RBAC başarısız olmadan ABAC’a veya CASL’a geçmek erkendir. Serinin ilerleyişi gerçek dünya evrimini yansıtır: basit başlayın, mevcut sınırlamalar ortaya çıktığında karmaşıklık ekleyin.
-
Multi-tenancy’yi sonradan düşünme: Şema oluşturulduktan sonra her tabloya
tenant_ideklemek zorlu bir migration’dır. İlk sürüm yalnızca bir tenant’a sahip olsa bile, tenant izolasyonunu baştan tasarlayın. -
Platform yöneticisini tenant yöneticisi ile karıştırma: Platform yöneticileri tüm tenant’ları yönetir (çapraz tenant erişimi). Tenant yöneticileri yalnızca kendi tenant’larını yönetir. Bu rolleri karıştırmak ya aşırı izin verici tenant yöneticileri ya da yetersiz izin verici platform yöneticileri oluşturur.
-
Harici PDP’yi çok erken seçme: Cerbos, OPA ve Cedar altyapı karmaşıklığı ekler. Monolitik bir Next.js uygulaması için süreç içi yetkilendirme (özel veya CASL) daha basit ve hızlıdır. Harici PDP’ler, yetkilendirme kararlarının bağımsız olarak dağıtılan servisler arasında paylaşılması gerektiğinde anlam kazanır.
-
Çapraz tenant senaryolarını test etmeme: Birim testleri genellikle tek bir tenant ID kullanır. Kullanıcı A’nın (tenant 1) Kullanıcı B’nin (tenant 2) dokümanına erişmeye çalıştığı açık test senaryoları ekleyin. Bu testler eksik tenant filtrelerini yakalar.
Seri Retrospektifi
Yedi Hedef Karnesi
Post 101 herhangi bir izin sistemi için yedi hedef belirledi. Her yaklaşımın puanlaması:
| Hedef | Dağınık (101) | Service Layer (102) | RBAC (103) | Özel ABAC (104-105) | CASL (106) |
|---|---|---|---|---|---|
| Yetkisiz erişimi önle | Kısmi | Evet | Evet | Evet | Evet |
| Tutarlı (tek doğruluk kaynağı) | Hayır | Evet | Evet | Evet | Evet |
| Otomatik zorunlu kılma | Hayır | Mimari | Mimari | Mimari + Global | Mimari |
| Güncellenmesi kolay | Hayır | Orta | Evet (matris) | Evet (builder) | Evet (kurallar) |
| Denetlenebilir | Hayır | Orta | Evet (matris) | Evet (builder) | Evet (kurallar) |
| Performanslı | Değişken | Evet + cache | Evet (O(1) arama) | Evet (koşul değerlendirme) | Evet (koşul değerlendirme) |
| Type-safe | Hayır | Kısmi | Tam | Tam | İyi |
Seri Mimari Evrimi
Altı yazı boyunca temel içgörü: Post 102’deki service layer hiçbir zaman değişmez. Her yetkilendirme yaklaşımı için zorunlu kılma noktasıdır. İçindeki karar motoru basit rol kontrollerinden RBAC’a, ABAC’a ve CASL’a evrilir, ama mimari sabit kalır. İlerlemeli yaklaşımı çalıştıran budur — her yükseltme service layer içinde kapsanır.
Mikroservis Yetkilendirmesi: İleriye Bakış
Seri monolitik bir Next.js uygulamasına odaklandı. Uygulamalar büyüdükçe, yetkilendirme kararları servis sınırları arasında çalışmalıdır. Üç pattern ortaya çıkar:
Pattern 1: Merkezi Yetkilendirme Servisi
Tek bir servis tüm izin kararlarını değerlendirir. Diğer servisler gRPC/HTTP ile çağrır. Tek doğruluk kaynağı, ama tek hata noktası ve her istekte ağ gecikmesi.
Pattern 2: Gömülü PDP (Sidecar)
Her mikroservis kendi policy engine’ini çalıştırır (OPA sidecar, Cerbos sidecar). Politikalar merkezi olarak yönetilir ve tüm sidecar’lara dağıtılır. Kararlar için ağ atlaması yok, ama politika senkronizasyonu karmaşıklığı ve sürüm kayması riski var.
Pattern 3: Token Tabanlı Claim’ler
Yetkilendirme verileri JWT claim’lerine gömülür (roller, izinler, tenantId). Servisler ek politika kontrolü olmadan token’a güvenir. En basit altyapı, ama eskimiş claim’ler ve kaynak düzeyinde yetkilendirme yok.
Monolitten mikroservislere geçen ekipler için: servisler arası auth için Pattern 3 (token claim’ler) ile başlayın, servisler arasında ayrıntılı kaynak düzeyinde yetkilendirme gerektiğinde Pattern 2 (gömülü PDP) ekleyin.
İzin Depolama: Kod vs. Veritabanı
| Yaklaşım | Güçlü Yönler | Zayıf Yönler | Ne Zaman Kullanmalı |
|---|---|---|---|
| Yalnızca kod (bu seri) | Type-safe, sürüm kontrollü, CI/CD ile test edilebilir | Değişiklikler için deployment gerektirir | İzin kuralları uygulama koduyla birlikte değişir |
| Veritabanında saklanan | Runtime yapılandırılabilir, tenant özelleştirilebilir | Derleme zamanı güvenliği yok, migration karmaşıklığı | Tenant’lar özel roller/izinler gerektirir |
| Hibrit | Kodda varsayılan kurallar + DB’de geçersiz kılmalar | İki sistemin karmaşıklığı, çakışma çözümü | Tenant başına özelleştirme gerektiren SaaS |
Hibrit pattern üretim SaaS için iyi çalışır: varsayılan izin setini kodda tanımlayın (type-safe, test edilmiş), tenant’ların belirli kuralları veritabanı tablosu ile geçersiz kılmasına izin verin. can() fonksiyonu önce kod tabanlı kuralları kontrol eder, sonra veritabanı geçersiz kılmalarını uygular.
Seri Özeti
Altı yazı boyunca izin sistemi, dağınık if ifadelerinden üretim kalitesinde bir yetkilendirme mimarisine evrildi:
- Post 101: Sorunu belirledi — dağınık kontroller, tutarsız zorunlu kılma, fail-closed varsayılanı yok
- Post 102: Mimariyi oluşturdu — tek zorunlu kılma noktası olarak service layer
- Post 103: İlk karar motorunu ekledi — generic constraint’lerle type-safe RBAC
- Post 104: Rol tabanlı aramayı attribute tabanlı politikalarla değiştirdi — sahiplik, departman, durum koşulları
- Post 105: ABAC’ı çevre kuralları, alan düzeyinde izinler ve veritabanı sorgu filtreleme ile genişletti
- Post 106 (bu yazı): Multi-tenancy ekledi, CASL’ı bir kütüphane alternatifi olarak değerlendirdi ve kesin karar çerçevesini sağladı
Service layer tek sabittir. İçindeki karar motoru RBAC, özel ABAC, CASL veya harici PDP olabilir. Post 102’deki mimari, yapısal değişiklik olmadan bunların hepsini destekler. Hedef başından beri buydu.
Kaynaklar
- CASL - Isomorphic Authorization JavaScript Library - Bu yazıda değerlendirilen temel yetkilendirme kütüphanesi, ABAC koşulları, alan düzeyinde izinler ve veritabanı sorgu dönüşümü desteği ile
- CASL v6 - Prisma Integration - CASL kurallarını Prisma
wherekoşullarına dönüştürenaccessibleBy()fonksiyonu için resmi dokümantasyon - CASL v6 - Restricting Fields Access -
permittedFieldsOf()ve kural tanımlarındaki alan dizileri hakkında resmi CASL dokümantasyonu - Shipping Multi-Tenant SaaS Using PostgreSQL Row-Level Security (Nile) - Tenant bağlam yayılımı ve fail-secure varsayılanları dahil multi-tenant SaaS için RLS uygulama rehberi
- The Developer’s Guide to SaaS Multi-Tenant Architecture (WorkOS) - İzolasyon gereksinimlerine ve tenant sayısına dayalı karar kriterleriyle multi-tenancy modellerine mimari genel bakış
- How to Choose the Right Authorization Model for Your SaaS (WorkOS) - Roller, izinler, ABAC, ReBAC ve politika tabanlı yetkilendirme arasında seçim yapmak için karar çerçevesi
- Policy Engines: OPA vs Cedar vs Zanzibar (Permit.io) - OPA (Rego tabanlı), Cedar (AWS, resmi doğrulama) ve Zanzibar (Google, graf tabanlı ReBAC) karşılaştırma analizi
- 3 Most Common Authorization Designs for SaaS Products (Cerbos) - ACL, RBAC ve ABAC pattern’lerinin her birinin ne zaman kullanılacağına dair rehberle karşılaştırması
- Multi-Tenant Data Isolation with PostgreSQL Row Level Security (AWS) - PostgreSQL’de tenant izolasyonu için RLS uygulama AWS rehberi
- Best Practices for Authorization in Microservices (Permit.io) - Merkezi ve gömülü PDP pattern’lerini ve önerilen sidecar mimarisini kapsayan rehber
- RBAC vs ABAC: Main Differences and When to Use Each (Oso) - Hibrit yaklaşımlar için karar kriterleriyle RBAC ve ABAC modellerinin karşılaştırması
- OWASP Microservices Security Cheat Sheet - Merkezi PDP ve gömülü PDP sidecar dahil mikroservis yetkilendirme pattern’leri hakkında OWASP rehberi
- An Introduction to Google Zanzibar and ReBAC (Authzed) - Google Zanzibar’ın ilişki tabanlı erişim kontrol modeli ve ölçekte yetkilendirmeyi nasıl desteklediğine genel bakış
Ölçeklenebilir İzin Sistemleri
TypeScript ve Next.js ile ölçeklenebilir izin sistemleri oluşturma rehberi. Basit kontrol mekanizmalarından RBAC ve ABAC'a, oradan multi-tenant yetkilendirme sistemlerine kadar kapsamlı bir seri.
Serideki tüm yazılar
İlgili yazılar
TypeScript'te builder pattern, koşullu izinler ve RBAC'ın sınırlarını aşan type-safe policy değerlendirmesi ile bir ABAC policy engine oluşturun.
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrarlanan izin mantığını ortadan kaldıran otomatik veritabanı sorgusu filtreleme ile genişletin.
Authentication ve authorization farkı, yaygın izin sistemi tuzakları, fail-closed prensibi ve her izin sisteminin karşılaması gereken hedefler.
Dağınık izin kontrollerini merkezi bir service layer'a taşıyın, Next.js middleware guard'ları ekleyin ve derinlemesine savunma yetkilendirme mimarisi oluşturun.
TypeScript ile type-safe bir RBAC sistemi oluşturun, birleşik bir can() fonksiyonu yazın, UI ve backend'de izinleri senkronize edin ve RBAC'ın sınırlarını anlayın.