İçeriğe atla

TypeScript'te Attribute-Based Access Control (ABAC): Policy Engine Oluşturma

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.

Ayhan Sipahi Ayhan Sipahi

Post 103 type-safe RBAC’ı can(role, resource, action) fonksiyonu ve deklaratif bir izin matrisi ile oluşturdu. Yeni bir rol eklemek tek satırlık bir değişiklik olarak kalıyor, sunucu ve istemci de tek bir doğruluk kaynağını paylaşıyor. Ancak bağlamsal kararlar (sahiplik, departman kapsamı, belge durumu) bu matrisin dışında özel yardımcı fonksiyonlar olarak kalıyor. canModifyDocument, canEditInDepartment ve canPublishInReviewStatus can() fonksiyonunun yanında çoğalıyor; her biri tip sisteminin doğrulayamadığı bir if/else zinciri.

Çözüm, bu koşulları policy’nin içine taşımak. ABAC (Attribute-Based Access Control), biçimsel tanımı NIST SP 800-162’de yapılan bir modeldir ve dört varlığın niteliklerini değerlendirir: özne (kullanıcı), kaynak (nesne), eylem (operasyon) ve ortam (bağlam). can(role, 'document', 'update') artı ayrı bir sahiplik kontrolü yerine, tek bir policy kararı ikisini birden kapsar.

TypeScript tarafında bu motorun karşılığı, builder pattern ile kurulan bir policy deposudur. Sahiplik, departman kapsamı ve kaynak durumu deklaratif kurallara dönüşür; jenerikler her kuralı ilgili kaynağın veri şekline karşı denetler. Post 102’deki service layer mimarisi değişmiyor; yalnızca can() imzası evriliyor.

ABAC Modeli#

Rollerden Niteliklere#

RBAC’ın temel sorusu: “Bu rol bu izne sahip mi?”

ABAC’ın temel sorusu: “Öznenin nitelikleri, kaynağın nitelikleri, bu eylem ve ortam göz önüne alındığında, policy erişime izin veriyor mu?”

Değişim: RBAC bir aramadır (rol -> izin seti -> evet/hayır). ABAC bir değerlendirmedir (nitelikler -> policy engine -> karar).

NIST SP 800-162: Dört Nitelik Kategorisi#

NIST Special Publication 800-162, ABAC’ı “yetkilendirmenin, özne, nesne, istenen operasyonlar ve bazı durumlarda ortam koşulları ile ilişkili niteliklerin değerlendirilmesiyle belirlenen mantıksal bir erişim kontrol metodolojisi” olarak tanımlar.

Dört kategori:

  1. Özne Nitelikleri: Erişim talep eden kullanıcının özellikleri: rol, departman, userId, yetki seviyesi. Bizim alanımızda: session.role, session.userId, session.departmentId.

  2. Kaynak (Nesne) Nitelikleri: Erişilen kaynağın özellikleri: sahip, durum, sınıflandırma. Bizim alanımızda: document.authorId, document.status, document.project.departmentId.

  3. Eylem Nitelikleri: Talep edilen operasyon: create, read, update, delete, publish. RBAC’ın eylemleriyle aynı, ancak ABAC belirli eylem-kaynak kombinasyonlarına koşullar ekleyebilir.

  4. Ortam Nitelikleri: Özne ve kaynağın dışındaki bağlamsal koşullar: geçerli zaman, IP adresi, feature flag’ler. Bizim alanımızda: çalışma saatleri, feature flag’ler.

ABAC Bir İsteği Nasıl Değerlendirir#

İzin Ver

Reddet

Erişim İsteği

Policy Enforcement Point

(Service Layer)

Policy Decision Point

(can() fonksiyonu)

Özne Nitelikleri

(rol, userId, departmentId)

Kaynak Nitelikleri

(authorId, status, projectId)

Eylem

(create, read, update, delete)

Ortam

(zaman, feature flag'ler)

Policy Değerlendirme

Karar

Devam Et

Reddet

Kurumsal ABAC’ta (XACML), mimari PEP, PDP, PIP (Policy Information Point) ve PAP (Policy Administration Point) bileşenlerini ayrı servisler olarak içerir. Uygulama seviyesinde TypeScript için, service layer PEP’tir ve can() fonksiyonu PDP’dir. Nitelikler zaten yüklenmiş session ve veritabanı nesnelerinden gelir; ayrı PIP/PAP’a gerek yoktur.

Bizim Alanımızda ABAC#

NIST modelinin Post 101-103’teki çalışan örneğe eşlenmesi:

NIST KavramıRBAC (Post 103)ABAC (Post 104)
Karar GirdisiYalnızca RolÖzne + Kaynak + Eylem + Ortam
Karar MantığıMap aramasıKoşul değerlendirme
”Kendi belgelerini düzenleyebilir mi?”can() dışında yardımcı fonksiyonKoşul: subject.userId === resource.authorId
”Yalnızca departmanda”can() dışında yardımcı fonksiyonKoşul: subject.departmentId === resource.departmentId
”Yalnızca taslak durumu”İfade edilemezKoşul: resource.status === 'draft'
Bağlamsal kural eklemekYeni yardımcı fonksiyon yazPolicy’ye bir koşul ekle

ABAC İçin TypeScript Tip Sistemi#

Kaynak Tipleri ve Veri Şekilleri#

RBAC’ta kaynaklar sadece string literal’lerdi ('document' | 'project'). ABAC, koşulların niteliklere type-safe şekilde başvurabilmesi için her kaynağın veri şeklini bilmek zorundadır.

// lib/permissions.ts

// Session tipi -- özne nitelikleri
interface User {
  userId: string;
  role: Role;
  departmentId?: string;
}

// Kaynak veri şekilleri -- her kaynağın hangi niteliklere sahip olduğu
interface ResourceDataMap {
  document: {
    authorId: string;
    status: 'draft' | 'published' | 'archived';
    projectId: string;
    departmentId: string;
  };
  project: {
    ownerId: string;
    departmentId: string;
    isArchived: boolean;
  };
}

export type Resource = keyof ResourceDataMap;

Post 103’e kıyasla tip seviyesindeki temel değişim budur. Resource hala bir string anahtarıdır, ancak bir veri tipine eşlenir. Bir policy “belgenin authorId’si kullanıcının userId’sine eşit olmalı” dediğinde, TypeScript authorId’nin belge tipinde gerçekten var olduğunu doğrulayabilir.

Tip Olarak Koşullar#

Koşullar, özne ve kaynak niteliklerini değerlendiren fonksiyonlardır. Tip sistemi, koşulların geçerli niteliklere başvurmasını sağlar:

// Koşul, kullanıcı ve kaynak verisini alır,
// true (izin ver) veya false (reddet) döner
type Condition<R extends Resource> = (
  user: User,
  data: ResourceDataMap[R]
) => boolean;

Jenerik parametre R extends Resource, koşulu belirli bir kaynak tipine bağlar. 'document' için bir koşul ResourceDataMap['document'] alır. TypeScript şeklin authorId, status, projectId, departmentId içerdiğini bilir. data.nonexistent referansı derleme zamanı hatasıdır.

Permission Store Tipi#

Permission store, Post 103’teki ROLE_PERMISSIONS nesnesinin yerine geçer. Rollerden izin string’lerine düz bir eşleme yerine, rolleri isteğe bağlı koşullarla kaynak-eylem çiftlerine eşler:

interface PermissionEntry<R extends Resource> {
  resource: R;
  actions: Action[];
  conditions?: Condition<R>[];
}

// Tam permission store: rol -> izin girdileri dizisi
type PermissionStore = {
  [R in Role]: PermissionEntry<Resource>[];
};

Her PermissionEntry şunu söyler: “Bu kaynak için bu eylemlere izin ver, isteğe bağlı olarak yalnızca bu koşullar sağlandığında.” conditions tanımsız veya boş ise izin koşulsuzdur (RBAC ile aynı). Koşullar varsa HEPSİ true olarak değerlendirilmelidir (AND mantığı).

RBAC Tipleri vs. ABAC Tipleri#

Evrimi görünür kılan yan yana karşılaştırma:

// Post 103 (RBAC) -- düz izin string'leri
type Permission = `${Resource}:${Action}`;
const ROLE_PERMISSIONS: Record<Role, readonly Permission[]>;
function can(role: Role, resource: Resource, action: Action): boolean;

// Post 104 (ABAC) -- kaynak verisi üzerinde koşullar
interface PermissionEntry<R extends Resource> {
  resource: R;
  actions: Action[];
  conditions?: Condition<R>[];
}
type PermissionStore = { [R in Role]: PermissionEntry<Resource>[] };
function can<R extends Resource>(
  user: User,
  action: Action,
  resource: R,
  data?: ResourceDataMap[R]
): boolean;

can() fonksiyonu artık tam kullanıcı nesnesini (sadece rol değil) ve isteğe bağlı olarak kaynak verisini alıyor. Bir iznin koşulları varsa, değerlendirme için veri gereklidir. Koşul yoksa (admin’in koşulsuz erişimi gibi), veri atlanabilir.

Permission Builder Pattern#

Neden Builder?#

PermissionStore nesnesini doğrudan oluşturmak hem uzun sürer hem de hataya açıktır:

// Builder olmadan -- uzun ve okuması zor
const store: PermissionStore = {
  admin: [
    { resource: 'document', actions: ['create', 'read', 'update', 'delete', 'publish'] },
    { resource: 'project', actions: ['create', 'read', 'update', 'delete'] },
  ],
  editor: [
    {
      resource: 'document',
      actions: ['create', 'read', 'update', 'publish'],
      conditions: [(user, doc) => user.departmentId === doc.departmentId],
    },
    { resource: 'project', actions: ['read'] },
  ],
  // ... daha fazla rol
};

Builder pattern, bir policy bildirimi gibi okunan akıcı bir API sağlar.

PermissionBuilder Sınıfı#

class PermissionBuilder {
  private store: PermissionStore = {
    admin: [],
    editor: [],
    author: [],
    viewer: [],
  };

  // Bir rol için izin tanımlamaya başla
  role(role: Role): RoleBuilder {
    return new RoleBuilder(this.store, role);
  }

  build(): PermissionStore {
    return this.store;
  }
}

class RoleBuilder {
  constructor(
    private store: PermissionStore,
    private currentRole: Role
  ) {}

  // Bir kaynak üzerinde izin ver, isteğe bağlı koşullarla
  can<R extends Resource>(
    actions: Action | Action[],
    resource: R,
    conditions?: Condition<R>[]
  ): RoleBuilder {
    const actionArray = Array.isArray(actions) ? actions : [actions];
    this.store[this.currentRole].push({
      resource,
      actions: actionArray,
      conditions,
    } as PermissionEntry<Resource>);
    return this;
  }

  // Başka bir role geç
  role(role: Role): RoleBuilder {
    return new RoleBuilder(this.store, role);
  }

  build(): PermissionStore {
    return this.store;
  }
}

Bu tasarımın önemli noktaları:

  • RoleBuilder.can() metodu R extends Resource üzerinde jeneriktir. Koşullar dizisi ilgili kaynağın tipine bağlanır, böylece TypeScript koşul fonksiyonlarının geçerli niteliklere başvurduğunu kontrol eder.
  • return this ile metot zincirleme akıcı API’yi mümkün kılar.
  • Builder pattern, nesnenin kurulumunu temsilinden ayırır. Son PermissionStore düz bir nesnedir, ancak kurulum süreci yönlendirilmiş ve tip kontrollüdür.

Builder Kullanımı#

const permissions = new PermissionBuilder()
  // Admin: tam erişim, koşul yok
  .role('admin')
    .can(['create', 'read', 'update', 'delete', 'publish'], 'document')
    .can(['create', 'read', 'update', 'delete'], 'project')

  // Editor: departman kapsamlı belge erişimi
  .role('editor')
    .can(['create', 'read', 'update', 'publish'], 'document', [
      (user, doc) => user.departmentId === doc.departmentId,
    ])
    .can('read', 'document') // herhangi bir belgeyi okuyabilir (koşul yok)
    .can('read', 'project')

  // Author: yalnızca kendi belgeleri
  .role('author')
    .can('create', 'document') // henüz sahiplik yok -- yeni belge
    .can(['read', 'update'], 'document', [
      (user, doc) => doc.authorId === user.userId,
    ])
    .can('read', 'project')

  // Viewer: yalnızca okuma
  .role('viewer')
    .can('read', 'document')
    .can('read', 'project')

  .build();

Zincir neredeyse düz cümle gibi okunur: “Bir author belge oluşturabilir. Bir author, belgenin yazarı kendisi ise belgeleri okuyabilir ve güncelleyebilir.” Post 103’te yardımcı fonksiyonlara dağılmış olan koşullar artık izin bildirimiyle aynı satırda duruyor.

Yeniden Yazılan can() Fonksiyonu#

Implementasyon#

function can<R extends Resource>(
  user: User,
  action: Action,
  resource: R,
  data?: ResourceDataMap[R]
): boolean {
  const entries = permissions[user.role] as PermissionEntry<R>[];

  for (const entry of entries) {
    if (entry.resource !== resource) continue;
    if (!entry.actions.includes(action)) continue;

    // Koşul yoksa izin verilir (koşulsuz)
    if (!entry.conditions || entry.conditions.length === 0) {
      return true;
    }

    // Koşullar var ama veri sağlanmadıysa bu girdiyi atla
    // (kaynak verisi olmadan koşullar değerlendirilemez)
    if (!data) continue;

    // Tüm koşullar geçmeli (AND mantığı)
    const allConditionsMet = entry.conditions.every(
      (condition) => condition(user, data)
    );

    if (allConditionsMet) return true;
  }

  return false; // Eşleşen girdi bulunamadı -- varsayılan olarak reddet
}

Bu koda gömülü birkaç tasarım kararı var:

  1. Varsayılan olarak reddet: Eşleşen girdi bulunamazsa fonksiyon false döner. Bu, Post 101’deki fail-closed ilkesidir.

  2. Koşulsuz izinler: conditions tanımsız veya boşsa, veri değerlendirilmeden izin verilir. Bu, admin tarzı erişimi yönetir; RBAC ile aynı davranış.

  3. Veriye bağlı izinler: Koşullar varsa kaynak verisi sağlanmalıdır. Veri eksikse bu girdi atlanır (genel olarak reddedilmez). Aynı kaynak-eylem çifti için başka bir girdi koşulsuz olabilir.

  4. Koşullar için AND mantığı: Bir girdideki tüm koşullar geçmelidir. “Editor, AYNI departman VE belge taslak ise belgeyi güncelleyebilir” her iki koşulun da doğru olmasını gerektirir. OR mantığı, aynı kaynak-eylem çifti için ayrı girdiler oluşturularak elde edilebilir.

  5. Tip çıkarımı: Jenerik R, resource 'document' olduğunda data’nın ResourceDataMap['document'] olması gerektiğini sağlar. TypeScript, veri şeklini derleme zamanında doğrular.

İmzanın Evrimi#

Seri boyunca can() fonksiyonunun imzası:

// Post 103 (RBAC):
can(role: Role, resource: Resource, action: Action): boolean

// Post 104 (ABAC):
can<R extends Resource>(
  user: User,
  action: Action,
  resource: R,
  data?: ResourceDataMap[R]
): boolean

Değişenler:

  • role artık user oldu: Tam kullanıcı nesnesi, policy’nin okuyabileceği her özne niteliğini taşır: rol, userId, departmentId.
  • data?: ResourceDataMap[R]: Koşul değerlendirme için isteğe bağlı kaynak verisi. Koşulsuz izinler (admin) buna ihtiyaç duymadığı için isteğe bağlıdır.
  • Jenerik R extends Resource: Kaynak string’ini veri tipine bağlayarak type-safe koşullar sağlar.

İsteğe Bağlı data Parametresi#

data neden zorunlu yerine isteğe bağlı?

Bir kaynak yüklenmeden önce izinlerin kontrol edilmesini düşünün. “Oluştur” butonu render edilirken henüz belge yok, geçilecek data yok. can() fonksiyonu koşulsuz kontroller için çalışmaya devam etmelidir.

// UI: "Oluştur butonunu gösterelim mi?"
// Henüz belge yok -- geçilecek veri yok
if (can(user, 'create', 'document')) {
  // Oluştur butonunu göster
}

// Servis: "Bu kullanıcı BU belgeyi güncelleyebilir mi?"
// Belge var -- koşul değerlendirmesi için verisini geç
if (can(user, 'update', 'document', {
  authorId: document.authorId,
  status: document.status,
  projectId: document.projectId,
  departmentId: document.project.departmentId,
})) {
  // güncellemeye devam et
}

Bu, fonksiyonu RBAC tarzı kontrollerle geriye dönük uyumlu yaparken ABAC tarzı koşullu kontrolleri de destekler.

Tüm Roller İçin Policy Tanımları#

Admin Policy#

.role('admin')
  .can(['create', 'read', 'update', 'delete', 'publish'], 'document')
  .can(['create', 'read', 'update', 'delete'], 'project')

Koşul yok. Admin, tüm kaynaklar üzerindeki tüm eylemlere koşulsuz erişime sahiptir. RBAC ile aynı, ancak artık birleşik policy engine içinde.

Editor Policy#

.role('editor')
  .can(['create', 'read', 'update', 'publish'], 'document', [
    (user, doc) => user.departmentId === doc.departmentId,
  ])
  .can('read', 'document') // herhangi bir belgeyi okuyabilir (koşul yok)
  .can('read', 'project')

Editor’ün belge için İKİ girdisi vardır. read eylemi hem koşullu (ilk girdide) hem koşulsuz (ikinci girdide) görünür. can() fonksiyonu ilk eşleşmede true döndüğünden, koşulsuz read girdisi herhangi bir belge için eşleşir. Koşullu girdi create, update ve publish için geçerlidir.

Post 103’te bu, ayrı bir canEditInDepartment() yardımcı fonksiyonu gerektiriyordu. Artık deklaratif bir koşuldur.

Author Policy#

.role('author')
  .can('create', 'document') // oluşturabilir (henüz sahiplik yok)
  .can(['read', 'update'], 'document', [
    (user, doc) => doc.authorId === user.userId,
  ])
  .can('read', 'project')

Author’lar belgeleri koşulsuz olarak oluşturabilir (yeni belgenin henüz yazarı yoktur). Yalnızca kendi belgelerini okuyabilir ve güncelleyebilir. Post 103’te yardımcı fonksiyon olan sahiplik kontrolü (document.authorId === session.userId) artık bir koşuldur.

Viewer Policy#

.role('viewer')
  .can('read', 'document')
  .can('read', 'project')

Koşul yok; yalnızca okuma erişimi ve RBAC ile birebir aynı davranış.

Policy Matrisi#

Post 103’teki RBAC izin tablosu koşulları içerecek şekilde evriliyor:

Kaynak:Eylemadmineditorauthorviewer
document:createevetevet (departman)evet
document:readevetevetevet (kendi)evet
document:updateevetevet (departman)evet (kendi)
document:deleteevet
document:publishevetevet (departman)
project:createevet
project:readevetevetevetevet
project:updateevet
project:deleteevet

“(departman)” ve “(kendi)” açıklamaları koşullardır. İzni kaynakların bir alt kümesiyle daraltırlar. Bu tablo hala incelenebilir ve denetlenebilir, ancak artık Post 103’ün yardımcı fonksiyonlarında gizli olan bağlamsal kuralları yakalar.

Service Layer ABAC Entegrasyonu#

Önce: RBAC + Yardımcı Fonksiyonlar (Post 103)#

// lib/services/document-service.ts
import 'server-only';
import { can, type Role } from '@/lib/permissions';

export async function updateDocument(
  documentId: string,
  content: string
) {
  const session = await requireSession();
  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: true },
  });

  if (!document) throw new NotFoundError();

  // RBAC kontrolü
  if (!can(session.role as Role, 'document', 'update')) {
    // Sahiplik kontrolü -- RBAC dışında
    if (document.authorId !== session.userId) {
      throw new ForbiddenError();
    }
  }

  // Departman kontrolü -- RBAC dışında başka bir yardımcı
  if (session.role !== 'admin' &&
      session.departmentId !== document.project.departmentId) {
    throw new ForbiddenError();
  }

  return await db.document.update({
    where: { id: documentId },
    data: { content },
  });
}

Tek bir metotta üç izin kontrolü üst üste biniyor. RBAC can() çağrısı rolü yönetir; sahiplik ve departman kapsamı ise elle yazılmış if bloklarıdır. Yeni bir bağlamsal kural eklemek, bir if bloğu daha eklemek demektir.

Sonra: Birleşik ABAC can() (Post 104)#

// lib/services/document-service.ts
import 'server-only';
import { can } from '@/lib/permissions';

export async function updateDocument(
  documentId: string,
  content: string
) {
  const session = await requireSession();
  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: true },
  });

  if (!document) throw new NotFoundError();

  // Tek ABAC kontrolü -- tüm koşullar içeride değerlendiriliyor
  if (!can(session, 'update', 'document', {
    authorId: document.authorId,
    status: document.status,
    projectId: document.projectId,
    departmentId: document.project.departmentId,
  })) {
    throw new ForbiddenError();
  }

  return await db.document.update({
    where: { id: documentId },
    data: { content },
  });
}

Değişenler:

  • Üç ayrı kontrol (RBAC can() + sahiplik + departman) tek bir can() çağrısına sığdı.
  • Kaynak verisi açıkça geçiliyor. TypeScript, şeklin ResourceDataMap['document'] ile eşleştiğini sağlıyor.
  • Yeni bir koşul eklemek (ör. “yalnızca taslak belgeler güncellenebilir”) policy builder’da gerçekleşir; bu servis metodu olduğu gibi kalır.

Daha Fazla Servis Metodu Örneği#

Belge yayınlama: editor departman kontrolü + author reddi tek bir kontrole dönüşüyor:

// Önce: RBAC + elle yazılan kontroller
export async function publishDocument(documentId: string) {
  const session = await requireSession();
  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: true },
  });
  if (!document) throw new NotFoundError();

  if (!can(session.role as Role, 'document', 'publish')) {
    throw new ForbiddenError();
  }
  // Departman kontrolü -- elle
  if (session.role !== 'admin' &&
      session.departmentId !== document.project.departmentId) {
    throw new ForbiddenError();
  }
  // ... yayınlama mantığı
}

// Sonra: tek ABAC kontrolü
export async function publishDocument(documentId: string) {
  const session = await requireSession();
  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: true },
  });
  if (!document) throw new NotFoundError();

  if (!can(session, 'publish', 'document', {
    authorId: document.authorId,
    status: document.status,
    projectId: document.projectId,
    departmentId: document.project.departmentId,
  })) {
    throw new ForbiddenError();
  }
  // ... yayınlama mantığı
}

Belge okuma: viewer koşulsuz + author sahipliği:

// Sonra: tek ABAC kontrolü
export async function getDocument(documentId: string) {
  const session = await requireSession();
  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: true },
  });
  if (!document) throw new NotFoundError();

  if (!can(session, 'read', 'document', {
    authorId: document.authorId,
    status: document.status,
    projectId: document.projectId,
    departmentId: document.project.departmentId,
  })) {
    throw new ForbiddenError();
  }

  return document;
}

Post 102’deki service layer yapısı değişmedi. Yalnızca izin kontrollerinin içindeki karar motoru evrim geçirdi.

Sık Yapılan Hatalar#

  1. Eksik kaynak verisi geçmek: Bir iznin koşulları varsa ama veri atlanırsa, koşullu girdi atlanır. Bu beklenmedik reddetmelere neden olabilir. Bir rolün izninin koşulları varsa, her zaman kaynak verisini geçin.

  2. AND vs. OR karışıklığı: Tek bir PermissionEntry üzerindeki birden çok koşul AND mantığı kullanır (hepsi geçmeli). OR mantığı için aynı kaynak-eylem çifti için ayrı girdiler oluşturun. Editor policy’si bunu gösterir: koşulsuz read ayrı bir girdidir, koşullu create/update/publish’den farklıdır.

  3. Yan etkili koşullar: Koşullar saf fonksiyonlar olmalıdır. Veritabanı çağrısı, mutasyon, loglama olmamalıdır. Önceden yüklenmiş veriyi alır ve boolean döner. Koşullardaki yan etkiler sistemi öngörülemez ve test edilemez kılar.

  4. Koşulsuz girdi önceliği: Bir rol aynı eylem-kaynak çifti için hem koşullu hem koşulsuz girdiye sahipse, koşulsuz olan ilk eşleşir (hemen true döner). Bu, editor’ün read erişimi için doğrudur, ancak girdi sırası düşünülmezse kafa karıştırıcı olabilir.

  5. Uygulama seviyesi ile kurumsal ABAC’ı karıştırmak: Kurumsal ABAC (XACML, OPA/Rego) ayrı PDP sunucuları ve ağ istekleri içerir. Uygulama seviyesi ABAC, bellek içi koşullarla yapılan yerel bir fonksiyon çağrısıdır. Policy engine, süreç içinde çalışan bir TypeScript modülüdür.

RBAC’tan ABAC’a Geçiş Faydaları#

Değişenler#

YönRBAC (Post 103)ABAC (Post 104)
Karar fonksiyonucan(role, resource, action) + yardımcılarcan(user, action, resource, data?)
Sahiplik kontrolücanModifyDocument() yardımcısıKoşul: doc.authorId === user.userId
Departman kapsamıcanEditInDepartment() yardımcısıKoşul: user.deptId === doc.deptId
Kaynak durum kontrolüİfade edilemezKoşul: doc.status === 'draft'
Bağlamsal kural eklemekYeni yardımcı fonksiyon + tüm çağrı noktalarını bulPolicy builder’a koşul ekle
Policy görünürlüğüİzin matrisi + dağınık yardımcılarHepsi policy builder’da
Koşulların tip güvenliğiYok (yardımcılar imperatif)Kaynağın tipine bağlı jenerik koşullar
Servis metodu değişiklikleriBir can() + birden çok if bloğuBir can() çağrısı

Aynı Kalanlar#

  • Service layer mimarisi (Post 102) değişmedi
  • verifySession() hala kullanıcı nesnesini sağlıyor
  • Service layer hala güvenlik sınırıdır
  • UI izin kontrolleri hala UX içindir, güvenlik için değil
  • Paylaşılan modül deseni (permissions.ts’de server-only yok) hala geçerli
  • Fail-closed ilkesi (Post 101) hala geçerli

Doğruluk Tabloları ile Test#

Policy’ler doğruluk tabloları olarak test edilebilir. Her test bağımsız bir nitelik kombinasyonudur:

// Test: author kendi belgesini güncelleyebilir
expect(can(
  { userId: 'u1', role: 'author' },
  'update',
  'document',
  { authorId: 'u1', status: 'draft', projectId: 'p1', departmentId: 'd1' }
)).toBe(true);

// Test: author başkasının belgesini güncelleyemez
expect(can(
  { userId: 'u1', role: 'author' },
  'update',
  'document',
  { authorId: 'u2', status: 'draft', projectId: 'p1', departmentId: 'd1' }
)).toBe(false);

// Test: admin herhangi bir belgeyi güncelleyebilir (veri gerekmez)
expect(can(
  { userId: 'u1', role: 'admin' },
  'update',
  'document'
)).toBe(true);

// Test: editor herhangi bir belgeyi okuyabilir (koşulsuz read)
expect(can(
  { userId: 'u1', role: 'editor', departmentId: 'd1' },
  'read',
  'document',
  { authorId: 'u2', status: 'published', projectId: 'p1', departmentId: 'd2' }
)).toBe(true);

// Test: editor farklı departmandaki belgeyi güncelleyemez
expect(can(
  { userId: 'u1', role: 'editor', departmentId: 'd1' },
  'update',
  'document',
  { authorId: 'u2', status: 'draft', projectId: 'p1', departmentId: 'd2' }
)).toBe(false);

Policy engine saf bir fonksiyon olduğu için bu testler mock, veritabanı veya sahte nesne gerektirmez. Her test bir nitelik seti verir ve beklenen boolean sonucu doğrular.

Karar Çerçevesi: RBAC vs. ABAC vs. Harici Engine#

Evet

Hayır

1-2 istisna

3+ bağlamsal kural

Hayır, tek uygulama

Evet, mikroservisler

Yetkilendirme Kararı

Gerekli

Karar yalnızca

kullanıcının rolüne

mi bağımlı?

RBAC (Post 103)

can(role, resource, action)

Kaç tane bağlamsal

kural var?

RBAC + Yardımcı Fonksiyonlar

(Post 103 yaklaşımı)

Servisler arası veya

dağıtık policy

değerlendirme

gerekli mi?

Uygulama Seviyesi ABAC

(Post 104)

Harici Policy Engine

(OPA, Cedar, Cerbos)

Post 106

KriterRBACABAC (Uygulama Seviyesi)Harici Engine
İzinlerin bağlı olduğu girdilerYalnızca rolRol + niteliklerRol + nitelikler + ilişkiler
Bağlamsal kural sayısı03-2020+ veya servisler arası
Policy’ler neredeKod (TypeScript map)Kod (builder/koşullar)Harici policy deposu
Değerlendirme yeriİşlem içi, senkronİşlem içi, senkronAğ çağrısı, asenkron
Tip güvenliğiTam (TypeScript)Tam (TypeScript jenerikler)Kısmi (policy dili)
Kontrol başına gecikmeMikrosaniyeMikrosaniyeMilisaniye (ağ)

Trade-off’lar#

Deklaratiflik vs. hata ayıklama: Deklaratif policy’ler imperatif yardımcı fonksiyonlardan daha kolay okunur, ancak bir izin beklenmedik şekilde reddedildiğinde, hangi koşulun başarısız olduğunu izlemek can() fonksiyonunda adım adım ilerlemeyi gerektirir. Geliştirme sırasında başarısız koşulu döndüren bir canWithReason() varyantı yardımcı olabilir.

Tip güvenliği vs. öğrenme eğrisi: TypeScript jenerikleri, koşulların geçerli kaynak niteliklerine başvurmasını zorunlu kılar. Bu, hataları derleme zamanında yakalar ancak tip tanımlarını daha karmaşık yapar. Jeneriklere aşina olmayan ekipler tipleri başlangıçta zorlayıcı bulabilir.

Merkezileştirilmiş policy vs. performans: Tüm policy’lerin tek bir builder’da olması görünürlük için harikadır. Ancak can() fonksiyonu her çağrıda tüm girdileri yineler. Basit uygulamalar için (4 rol, rol başına 5-10 girdi) bu mikrosaniyelerdir. Karmaşık policy’ler için optimizasyon düşünülebilir (Post 105 bunu ele alır).

Uygulama seviyesi vs. harici engine: TypeScript’te yazılan ABAC hızlı ve type-safe’tir, ağ yükü getirmez. Ancak policy’ler uygulama koduna bağlıdır ve değişiklik için deployment gerekir. Harici engine’ler (OPA, Cedar) policy’leri koddan ayırır ancak gecikme ve operasyonel karmaşıklık ekler (Post 106 bu kararı kapsar).

Sonraki Adım#

Uygulama seviyesi ABAC, her koşul bellekte zaten yüklü nesneler üzerinde değerlendirilebildiği sürece geçerlidir. Bu sınırın ötesinde iki kısıt beliriyor: bir kullanıcının hangi alanları görebileceğini veya değiştirebileceğini denetlemek ve koşulları veritabanına iterek sorguların yalnızca izin verilen satırları döndürmesini sağlamak.

Post 105’te ABAC engine alan seviyesi izinlere ve veritabanı entegrasyonuna genişliyor. Koşullar ORM uyumlu where cümlelerine dönüştürülüyor, böylece izinler sorgu seviyesinde veritabanı tarafından uygulanıyor.

// Önizleme -- tam implementasyon Post 105'te
// Alan seviyesi: bu kullanıcı hangi alanları görebilir?
const fields = getVisibleFields(session, 'document');
// -> ['title', 'content'] author için
// -> ['title', 'content', 'internalNotes'] admin için

// Veritabanı seviyesi: koşulları sorguya it
const documents = await db.document.findMany({
  where: toWhereClause(session, 'document', 'read'),
  // -> { departmentId: session.departmentId } editor için
  // -> { authorId: session.userId } author için
  // -> {} admin için
});

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.

İlerleme 4/6 yazı tamamlandı

İlgili yazılar