TypeScript'te Multi-Tenant Yetkilendirme: CASL vs Özel ABAC
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini karar çerçeveleriyle seçin.
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.
Üretime çıkmadan önce üç sorun kalıyor. Sistemin tenant sınırı yok: Organizasyon A’daki bir kullanıcı, sorgu doğru kurgulanırsa Organizasyon B’nin kaynaklarına erişebilir; bu izolasyon her service metoduna değil, izin katmanına global bir koşul olarak yerleşmeli. Özel ABAC motoru çalışıyor ama ekibin bu kodu sürdürmeye devam mı edeceği yoksa CASL gibi bir kütüphaneye mi geçeceği belirsiz; motor, ekip bu kodu sahiplenmekten vazgeçene kadar yerinde kalır ve CASL’ın yerini kazandığı nokta tam olarak burasıdır. RBAC, özel ABAC, kütüphane tabanlı ABAC ve harici policy engine arasında seçim yapmayı sağlayan bir karar çerçevesi de hâlâ yok; bu seçim, her kaynağın kaç bağlamsal koşula ihtiyaç duyduğuna göre belirlenir.
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. Sistem; rolleri, sahipliği veya alan erişimini kontrol etmeden önce 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. Birden fazla tenant’a üyelik mümkün ama bu kapsamın dışına taşan bir karmaşıklık getiriyor.
Üç İ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 Sınırını Bilen İzin Katmanı#
Dağınık Tenant Kontrolü Anti-Pattern’i#
İzin katmanı tenant sınırını bilmediğinde, her service metodu tenant izolasyonunu elle 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. Tek bir endpoint’te tenant kontrolünün unutulması çapraz tenant veri sızıntısı yaratır; bu, diğer müşterilerin verilerini açığa çıkardığı 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şim İstisnaları#
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.
Kütüphane Kullanım Gerekçesi#
Kendin Yazmak mı, Kütüphane mi?#
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?
Her İki Seçeneğin Bedeli#
Kendi kodunu yazmak, kritik güvenlik yolunda sıfır bağımlılık, can() imzası üzerinde tam kontrol ve domain’e özel tasarlanmış bir TypeScript entegrasyonu (generic constraint’ler, builder pattern’ler, type inference) sağlar. Hiçbir şey serileştirilmez: düz fonksiyonlar, class instance’ı yok, doğası gereği RSC uyumlu. Her koşul ekibin yazdığı bir fonksiyon olduğu için ortada kara kutu yoktur ve hata ayıklama normal bir çağrı yığınını takip eder. Ama bedeli de aynı ekibin üzerindedir: her hatayı, uç durumu ve güvenlik yamasını kendisi üstlenir, yaygın kullanılan bir kütüphanenin biriktirdiği topluluk testinden yoksun kalır; alan izinleri, DB sorgu dönüşümü ve koşul operatörleri ($in, $ne, $gte) sıfırdan yeniden yazılır ve yeni ekip üyeleri belgelenmiş bir API yerine özel bir API öğrenir.
Bir izin kütüphanesi, binlerce projede topluluk tarafından test edilmiş halde gelir; alan izinleri, MongoDB tarzı koşullar ve Prisma/Mongoose adaptörleri zaten hazırdır, dokümantasyon, Stack Overflow yanıtları ve konferans sunumları mevcuttur çünkü ekibin bunları kendisi yazması gerekmemiştir. Bedeli kontroldür: kütüphanenin API’si serinin can() imzasıyla eşleşmeyebilir, bakımı yavaşlayabilir veya durabilir, class tabanlı tasarımlar React Server Components ile çakışır ve bir izin reddini debug etmek, ekibin yazdığı bir fonksiyon yerine başkasının iç yapısını anlamayı gerektirir.
Karar Çerçevesi: Kendin Yazmak mı, Kütüphane mi?#
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#
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 MongoDB tarzı nesnelerdir (
{ authorId: user.userId }); özel builder fonksiyon kullanıyordu - 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 Getirdiği Kısıtlar#
CASL, kontrol edilen nesnenin türünü bilmek zorundadır. Class kullanıldığında bunu class adından çıkarır. TypeScript uygulamalarının genellikle tercih ettiği düz nesnelerde ise subject() helper’ı gerekir:
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 trade-off 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 ve bir kuralda eksik bırakmak çapraz tenant sızıntısı oluşturur.
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 uyumlu PureAbility pattern’ini kullanıyorsanız, Prisma sorgu dönüşümünü kaybedersiniz.
Dört Yaklaşım Arasında Seçim#
RBAC vs. Özel ABAC vs. CASL ABAC#
| Boyut | RBAC (Post 103) | Özel ABAC (Post 104-105) | CASL ABAC (Post 106) |
|---|---|---|---|
| Ç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 |
Sistem Seçim Kılavuzu#
Her Birini Ne Zaman Seçmeli#
RBAC (Post 103)
Her ekip boyutunda çalışır; dahili araçlar, basit SaaS ve net rollere sahip içerik platformları için uygundur: 2-4 rol, izinler yalnızca role bağlıdır. İzin gereksinimleri “bu rol bu şeyleri yapabilir” şeklinde temiz bir eşleme oluşturduğunda seçilir.
Yetkilendirme uzmanlığına sahip ve auth kodunu sürdürmeye istekli bir ekip, karmaşık iş kurallarına sahip SaaS, alan düzeyinde görünürlük ve büyük veri kümeleri için bu seçeneğe yönelir: sahiplik, departman, durum ve zaman koşulları can() içine girer; bu noktada can() kaynak başına 3+ bağlamsal koşulu değerlendirir. Bu denge, auth bakımı için ekip bant genişliği azaldığında veya birden fazla ORM’de DB sorgu adaptörlerine ihtiyaç duyulduğunda değişir.
CASL ABAC (Post 106)
CASL, zamanını auth iç yapısı yerine iş mantığına ayırmayı tercih eden bir ekibe uyar: Prisma/MongoDB kullanan, alan izinleri ile DB filtreleme gerektiren SaaS kurarken özel kodu, özel ABAC özellik seti CASL’ın yetenekleriyle zaten eşleştiği anda tercih ettiği bir kütüphane API’siyle değiştirir. Düz nesnelerle yoğun RSC kullanımı, çevre koşulları gereksinimi veya global tenant izolasyonu gereksinimi bu seçeneği devre dışı bırakır.
Harici PDP (Cerbos, OPA, Cedar)
Bu seçenekten en çok fayda, genellikle mikroservis veya polyglot bir stack’te çalışan adanmış bir platform veya güvenlik ekibi çıkarır; birden fazla servisin tutarlı yetkilendirme uygulaması gerektiği ve uyumluluğun ayrıştırılmış, denetlenebilir bir politika yönetimi istediği durumlarda bu seçeneğe yönelin.
Multi-Tenant Yetkilendirme Nerede Kırılır#
-
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.
-
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.
Serinin Değerlendirmesi#
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 |
Serinin Mimari Evrimi#
Post 102’deki service layer, altı yazı boyunca hiçbir zaman değişmez. İçindeki karar motoru basit rol kontrollerinden RBAC’a, ABAC’a ve CASL’a evrilirken bu katman her yetkilendirme yaklaşımı için zorunlu kılma noktası olarak kalır; böylece her yükseltme tek bir katmanın içinde sınırlı kalır.
Servis Sınırları Arasında Yetkilendirme#
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; bu genellikle üç pattern’den biriyle olur:
| Pattern | Mekanizma | Trade-off |
|---|---|---|
| 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 |
| Gömülü PDP (Sidecar) | Her mikroservis kendi policy engine’ini çalıştırır (OPA sidecar, Cerbos sidecar), bir politika yöneticisi tarafından merkezi olarak senkronize edilir | Kararlar için ağ atlaması yok, ama politika senkronizasyonu karmaşıklığı ve sürüm kayması riski var |
| Token Tabanlı Claim’ler | Yetkilendirme verileri JWT claim’lerine gömülür (roller, izinler, tenantId); servisler ek 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.
İzinleri Nerede Saklamalı: Kod mu, Veritabanı mı?#
| 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: Multi-tenancy ekledi, CASL’ı bir kütüphane alternatifi olarak değerlendirdi ve karar çerçevesini ortaya koydu
İzin kuralları rollerle temiz biçimde eşleştiği sürece varsayılan RBAC’tır; tek bir kaynak can() içinde üç veya daha fazla bağlamsal koşul gerektirmeye başladığında bir sonraki adıma geçilir.
Kaynaklar#
- CASL - Isomorphic Authorization JavaScript Library (yeni sekmede açılır) - CASL’ın kaynak deposu; ABAC koşulları, alan düzeyinde izinler ve veritabanı sorgu dönüşümü desteğiyle
- CASL v7 - Prisma Integration (yeni sekmede açılır) - CASL kurallarını Prisma
wherekoşullarına dönüştürenaccessibleBy()fonksiyonu için resmi dokümantasyon - CASL v7 - Restricting Fields Access (yeni sekmede açılır) -
permittedFieldsOf()ve kural tanımlarındaki alan dizileri hakkında resmi CASL dokümantasyonu - Shipping Multi-Tenant SaaS Using PostgreSQL Row-Level Security (Nile) (yeni sekmede açılır) - 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) (yeni sekmede açılır) - İ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) (yeni sekmede açılır) - 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) (yeni sekmede açılır) - 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) (yeni sekmede açılır) - 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) (yeni sekmede açılır) - PostgreSQL’de tenant izolasyonu için RLS uygulama AWS rehberi
- Best Practices for Authorization in Microservices (Permit.io) (yeni sekmede açılır) - 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) (yeni sekmede açılır) - Hibrit yaklaşımlar için karar kriterleriyle RBAC ve ABAC modellerinin karşılaştırması
- OWASP Microservices Security Cheat Sheet (yeni sekmede açılır) - 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) (yeni sekmede açılır) - 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.
Bu serideki tüm yazılar
İlgili yazılar
AWS Cognito ve Verified Permissions ile SaaS yetkilendirmeyi Cedar politikaları, çok kiracılı desenler, JWT akışı ve maliyet analiziyle TypeScript'te kurun.
authorization · aws · authentication +4
Authentication ve authorization farkı, yaygın izin sistemi tuzakları, fail-closed prensibi ve her izin sisteminin karşılaması gereken hedefler.
typescript · nextjs · authorization +2
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 · nextjs · authorization +2
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.
typescript · nextjs · authorization +2
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.
typescript · nextjs · authorization +2