Skip to content
~/sph.sh

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:

typescript
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.

BoyutSatır DüzeyiŞema DüzeyiVeritabanı Düzeyi
Altyapı maliyetiDüşükOrtaYüksek
İzolasyon gücüUygulama düzeyindeDB-şema düzeyindeFiziksel izolasyon
Tenant sayısı ölçeklenebilirliğiBinlerce+YüzlerceOnlarca
Migration karmaşıklığıTek migrationN migrationN migration + N veritabanı
Çapraz tenant sorgu riskiYüksek (eksik WHERE)Düşük (yanlış şema = hata)Yok
Tenant başına özelleştirmeSınırlıOrtaTam
Uyumluluk uygunluğuStandartSOC 2 / ISOHIPAA / 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:

typescript
// 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:

typescript
// Tenant izolasyonunu yerleşik global koşul olarak tanımlamaconst 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:

typescript
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:

typescript
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
typescript
// 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 yokinterface 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

  1. Sıfır bağımlılık: Kritik güvenlik yolunda üçüncü taraf kodu yok
  2. API yüzeyinde tam kontrol: can() imzası tam olarak gerektiği gibi gelişir
  3. Mükemmel TypeScript entegrasyonu: Generic constraint'ler, builder pattern'ler ve spesifik domain için tasarlanmış type inference
  4. Serileştirme yükü yok: Düz fonksiyonlar, class instance'ları yok, RSC uyumlu
  5. Ekip anlayışı: Her koşul ekibin yazdığı bir fonksiyon -- kara kutu yok
  6. Öngörülebilir davranış: Hata ayıklama standart fonksiyon çağrı yığınlarını takip eder

Özel Uygulama Zayıf Yönleri

  1. Bakım yükü: Ekip hataları, uç durumları ve güvenlik yamalarını sahiplenir
  2. Sınırlı topluluk testi: Olağandışı uç durumlar üretime kadar ortaya çıkmayabilir
  3. Ö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
  4. Onboarding maliyeti: Yeni ekip üyeleri belgelenmiş bir kütüphane yerine özel bir API öğrenir

Kütüphane Güçlü Yönleri

  1. Topluluk tarafından test edilmiş: Binlerce proje, keşfedilen ve düzeltilen uç durumlar
  2. Yerleşik özellikler: Alan izinleri, MongoDB tarzı koşullar, Prisma/Mongoose adaptörleri
  3. Dokümantasyon ve topluluk: Öğreticiler, Stack Overflow yanıtları, konferans sunumları
  4. Azaltılmış bakım: Güvenlik yamaları ve özellik eklemeleri bakımcılar tarafından yönetilir

Kütüphane Zayıf Yönleri

  1. API kısıtlamaları: Kütüphanenin API'si serinin can() imzasıyla eşleşmeyebilir
  2. Bağımlılık riski: Kütüphane bakımı yavaşlayabilir veya durabilir
  3. Entegrasyon sürtünmesi: Class tabanlı kütüphaneler React Server Components ile çakışır
  4. 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ÖrneklerGüçlü YönlerZayıf Yönler
In-code (TypeScript)Özel, CASLType-safe, runtime yükü yok, tanıdık dilDeployment'a bağlı, runtime değişikliği yok
DSL / Policy diliOPA/Rego, Cedar, Cerbos (YAML)Uygulama kodundan ayrık, geliştirici olmayanlar düzenleyebilir, denetlenebilirÖğrenme eğrisi, araç yükü, gecikme
HibritPermit.io, özel DB'de saklanan kurallarRuntime yapılandırılabilir + kod tabanlı varsayılanlarKarmaşı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.

bash
npm install @casl/ability @casl/prisma

Migration: AbilityBuilder

Post 104'teki özel PermissionBuilder, CASL'ın AbilityBuilder'ına eşlenir:

Özel (Post 104-105):

typescript
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ığı:

typescript
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/else dallanma 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:

typescript
import { subject } from '@casl/ability';
// CASL düz nesnelerin sarmalanmasını gerektirirability.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

typescript
// Orijinali mutasyona uğratmamak için kopya oluşturability.can('update', subject('Document', { ...document }));

Geçici çözüm 2: Özel detectSubjectType

typescript
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 eklefunction toDocumentDTO(doc: Document): DocumentDTO & { __type: 'Document' } {  return { ...doc, __type: 'Document' };}

Geçici çözüm 3: Lambda matcher ile PureAbility (RSC uyumlu)

typescript
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

typescript
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

typescript
import { permittedFieldsOf } from '@casl/ability/extra';
// Alan düzeyinde kurallar tanımlacan('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ı alconst 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ü

typescript
import { accessibleBy } from '@casl/prisma';
// CASL kurallarını Prisma where koşuluna dönüştürconst documents = await prisma.document.findMany({  where: accessibleBy(ability).Document,});
// İş mantığı filtreleriyle birleştirconst 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ı Prisma where sözdizimine dönüştürür
  • Özel toWhereClause(), toFilter callback'leri olan koşul tanımlayıcılarını kullanır
  • CASL, birden fazla eşleşen kural arasında OR mantığını otomatik olarak yönetir
  • CASL, hiçbir kural eşleşmezse ForbiddenError fı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. Bu, mevcut CASL mimarisinde kesin bir ödünleşimdir.

Kapsamlı Karşılaştırma

RBAC vs. Özel ABAC vs. CASL ABAC

BoyutRBAC (Post 103)Özel ABAC (Post 104-105)CASL ABAC (Bu Yazı)
Çekirdek mantıkRol-izin aramasıKoşullu policy engineMongoDB 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şullarHayır (helper gerektirir)Evet (policy builder'da inline)Evet (MongoDB tarzı nesneler veya lambda'lar)
Alan düzeyinde izinlerHayırEvet (getVisibleFields, pickPermittedFields)Evet (permittedFieldsOf)
DB sorgu filtrelemeHayırEvet (toWhereClause())Evet (accessibleBy() ile Prisma)
Çevre kurallarıHayırEvet (zaman, IP, flag'ler)Kısmi (özel koşullar ile)
Multi-tenancyMetod başına manuel kontrolGlobal koşul (otomatik)Kural başına tenantId (kural başına manuel)
Type safetyTam (generic'ler, mapped type'lar)Tam (resource-action generic'ler)İyi (typed action/subject, koşullarda daha zayıf)
RSC uyumluluğuTam (düz fonksiyonlar)Tam (düz fonksiyonlar)Kısmi (subject() mutasyon sorunu)
Negatif kurallarHayırHayırEvet (cannot())
BakımEkip sahipliğindeEkip sahipliğindeKütüphane bakımlı çekirdek
Bundle boyutu0 (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

  1. Bir CASL kuralında tenant izolasyonunu unutma: CASL'da global koşul yoktur. Bir kuralda tenantId eksik bırakmak çapraz tenant sızıntısı oluşturur. Her platform-admin olmayan kuralın tenantId içerdiğini doğrulayan bir lint kuralı veya birim testi yazın.

  2. 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.

  3. PureAbility Prisma entegrasyonunu kaybeder: RSC uyumlu PureAbility + lambda matcher pattern'i koşulları Prisma where koşullarına dönüştüremez. Ekipler RSC uyumluluğu ile DB sorgu filtreleme arasında seçim yapmalıdır.

  4. 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.

  5. Multi-tenancy'yi sonradan düşünme: Şema oluşturulduktan sonra her tabloya tenant_id eklemek zorlu bir migration'dır. İlk sürüm yalnızca bir tenant'a sahip olsa bile, tenant izolasyonunu baştan tasarlayın.

  6. 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.

  7. 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.

  8. Ç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ı:

HedefDağınık (101)Service Layer (102)RBAC (103)Özel ABAC (104-105)CASL (106)
Yetkisiz erişimi önleKısmiEvetEvetEvetEvet
Tutarlı (tek doğruluk kaynağı)HayırEvetEvetEvetEvet
Otomatik zorunlu kılmaHayırMimariMimariMimari + GlobalMimari
Güncellenmesi kolayHayırOrtaEvet (matris)Evet (builder)Evet (kurallar)
DenetlenebilirHayırOrtaEvet (matris)Evet (builder)Evet (kurallar)
PerformanslıDeğişkenEvet + cacheEvet (O(1) arama)Evet (koşul değerlendirme)Evet (koşul değerlendirme)
Type-safeHayırKısmiTamTamİ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şımGüçlü YönlerZayıf YönlerNe Zaman Kullanmalı
Yalnızca kod (bu seri)Type-safe, sürüm kontrollü, CI/CD ile test edilebilirDeğişiklikler için deployment gerektirirİzin kuralları uygulama koduyla birlikte değişir
Veritabanında saklananRuntime yapılandırılabilir, tenant özelleştirilebilirDerleme zamanı güvenliği yok, migration karmaşıklığıTenant'lar özel roller/izinler gerektirir
HibritKodda 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:

  1. Post 101: Sorunu belirledi -- dağınık kontroller, tutarsız zorunlu kılma, fail-closed varsayılanı yok
  2. Post 102: Mimariyi oluşturdu -- tek zorunlu kılma noktası olarak service layer
  3. Post 103: İlk karar motorunu ekledi -- generic constraint'lerle type-safe RBAC
  4. Post 104: Rol tabanlı aramayı attribute tabanlı politikalarla değiştirdi -- sahiplik, departman, durum koşulları
  5. Post 105: ABAC'ı çevre kuralları, alan düzeyinde izinler ve veritabanı sorgu filtreleme ile genişletti
  6. 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

Ö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.

İlerleme6/6 yazı tamamlandı

İlgili Yazılar