İçeriğe atla

2026-03-15

ABAC: Policy Engine ile Attribute Bazlı Erişim Kontrolü

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.

Özet

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. Sunucu ve istemci tek bir doğruluk kaynağını paylaşıyor.

Ancak bağlamsal kararlar (sahiplik, departman kapsamı, belge durumu) RBAC dışında özel yardımcı fonksiyonlar olarak kalmaya devam ediyor. canModifyDocument, canEditInDepartment, canPublishInReviewStatus gibi fonksiyonlar can() fonksiyonunun yanında çoğalıyor. Her biri tip sisteminin doğrulayamadığı özel bir if/else zinciri.

ABAC (Attribute-Based Access Control), NIST SP 800-162 tarafından biçimlendirilmiş olup 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, ABAC her şeyi tek bir policy kararında değerlendirir.

Bu yazı builder pattern ile type-safe bir policy engine oluşturuyor. Sahiplik, departman kapsamı ve kaynak durumu deklaratif policy kurallarına dönüşüyor. Post 102’deki service layer mimarisi değişmiyor. can() fonksiyonunun 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

Izin Ver

Reddet

Erisim Istegi

Policy Enforcement Point

(Service Layer)

Policy Decision Point

(can() fonksiyonu)

Ozne Nitelikleri

(rol, userId, departmentId)

Kaynak Nitelikleri

(authorId, status, projectId)

Eylem

(create, read, update, delete)

Ortam

(zaman, feature flag'ler)

Policy Degerlendirme

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 (Bu Yazı)
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

Veri Şekilli Kaynak Tipleri

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;

Bu, Post 103’ten temel tip seviyesi değişimidir. 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 ayrıntılı ve hataya açıktır:

// Builder olmadan -- ayrıntılı, 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 belirli kaynağa tiplidir, böylece TypeScript koşul fonksiyonlarının geçerli niteliklere başvurduğunu kontrol eder.
  • return this ile metod zincirleme akıcı API’yi mümkün kılar.
  • Builder pattern yapılandırmayı temsilden ayırır. Son PermissionStore düz bir nesnedir, ancak yapılandırma 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();

Bu İngilizce gibi okunur: “Bir author belge oluşturabilir (can create). Bir author, belgenin yazarı kullanıcı ise belgeleri okuyabilir ve güncelleyebilir.” Post 103’te yardımcı fonksiyonlara dağılmış olan koşullar artık izin bildirimi ile birlikte satır içindedir.

Yeniden Yazılan can() Fonksiyonu

Uygulama

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 uygulamaya gömülü birkaç tasarım kararı:

  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.

İmza 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 tüm özne niteliklerini (rol, userId, departmentId) sağlar, yalnızca rolü değil.
  • 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. RBAC ile 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 },
  });
}

Birden çok izin kontrolü. RBAC can() kontrolü rolü yönetir. Sahiplik kontrolü manüeldir. Departman kontrolü manüeldir. Yeni bir bağlamsal kural eklemek başka bir if bloğu 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’a koşul eklemek demek, bu servis metodunu değiştirmek değil.

Daha Fazla Servis Metodu Örneği

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

// Önce: RBAC + manüel 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ü -- manüel
  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 evrimi 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 (bu yazı) bellek içi koşullarla yerel bir fonksiyon çağrısıdır. Policy engine bir TypeScript modülüdür, dağıtık bir servis değil.

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 zorunludur)Kaynağa tipli 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);

Mock yok, veritabanı yok, yan etki yok. Policy engine saf bir fonksiyondur. Her test bir nitelik seti sağlar ve beklenen boolean sonucu doğrular.

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

Evet

Hayir

1-2 istisna

3+ baglamsal kural

Hayir, tek uygulama

Evet, mikroservisler

Yetkilendirme Karari

Gerekli

Karar yalnizca

kullanicinin rolune

mi bagimli?

RBAC (Post 103)

can(role, resource, action)

Kac tane baglamsal

kural var?

RBAC + Yardimci Fonksiyonlar

(Post 103 yaklasimi)

Servisler arasi veya

dagitik policy

degerlendirme

gerekli mi?

Uygulama Seviyesi ABAC

(Bu Yazi)

Harici Policy Engine

(OPA, Cedar, Cerbos)

Post 106

KriterRBACABAC (Uygulama Seviyesi)Harici Engine
İzinler bağımlıYalnı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ğ)

Ödünleşimler

Deklaratiflik vs. hata ayıklama: Deklaratif policy’ler zorunlu yardımcılardan 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 takımlar 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: Bu yazı ABAC’ı TypeScript’te oluşturur: hızlı, type-safe, sıfır ağ yükü. 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).

Sırada Ne Var

ABAC policy engine, özne/kaynak/eylem/ortam değerlendirmesini tek bir can() fonksiyonu ile yönetiyor. Sahiplik, departman kapsamı ve kaynak durumu artık deklaratif policy kurallarıdır. Post 103’teki dağınık yardımcı fonksiyonlar ortadan kalktı.

Ancak tüm koşullar uygulama belleğinde, zaten yüklenmiş nesneler üzerinde değerlendirilir. Peki bir kullanıcının hangi alanları görebileceğini veya değiştirebileceğini kontrol etmek? ABAC koşullarını veritabanına itmek, böylece sorguların yalnızca izin verilen verileri döndürmesi?

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 veritabanı izinleri sorgu seviyesinde uyguluyor, yalnızca uygulama mantığında değil.

// Önizleme -- tam uygulama 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ı

İlgili yazılar