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:
-
Ö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. -
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. -
Eylem Nitelikleri: Talep edilen operasyon: create, read, update, delete, publish. RBAC’ın eylemleriyle aynı, ancak ABAC belirli eylem-kaynak kombinasyonlarına koşullar ekleyebilir.
-
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
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 Girdisi | Yalnızca Rol | Özne + Kaynak + Eylem + Ortam |
| Karar Mantığı | Map araması | Koşul değerlendirme |
| ”Kendi belgelerini düzenleyebilir mi?” | can() dışında yardımcı fonksiyon | Koşul: subject.userId === resource.authorId |
| ”Yalnızca departmanda” | can() dışında yardımcı fonksiyon | Koşul: subject.departmentId === resource.departmentId |
| ”Yalnızca taslak durumu” | İfade edilemez | Koşul: resource.status === 'draft' |
| Bağlamsal kural eklemek | Yeni yardımcı fonksiyon yaz | Policy’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()metoduR 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 thisile metod zincirleme akıcı API’yi mümkün kılar.- Builder pattern yapılandırmayı temsilden ayırır. Son
PermissionStoredü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ı:
-
Varsayılan olarak reddet: Eşleşen girdi bulunamazsa fonksiyon
falsedöner. Bu, Post 101’deki fail-closed ilkesidir. -
Koşulsuz izinler:
conditionstanımsız veya boşsa, veri değerlendirilmeden izin verilir. Bu, admin tarzı erişimi yönetir; RBAC ile aynı davranış. -
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.
-
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.
-
Tip çıkarımı: Jenerik
R,resource'document'olduğundadata’nınResourceDataMap['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:
roleartıkuseroldu: 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:Eylem | admin | editor | author | viewer |
|---|---|---|---|---|
| document:create | evet | evet (departman) | evet | — |
| document:read | evet | evet | evet (kendi) | evet |
| document:update | evet | evet (departman) | evet (kendi) | — |
| document:delete | evet | — | — | — |
| document:publish | evet | evet (departman) | — | — |
| project:create | evet | — | — | — |
| project:read | evet | evet | evet | evet |
| project:update | evet | — | — | — |
| project:delete | evet | — | — | — |
“(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 bircan()ç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
-
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.
-
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şulsuzreadayrı bir girdidir, koşullucreate/update/publish’den farklıdır. -
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.
-
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
truedöner). Bu, editor’ünreaderişimi için doğrudur, ancak girdi sırası düşünülmezse kafa karıştırıcı olabilir. -
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ön | RBAC (Post 103) | ABAC (Post 104) |
|---|---|---|
| Karar fonksiyonu | can(role, resource, action) + yardımcılar | can(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 edilemez | Koşul: doc.status === 'draft' |
| Bağlamsal kural eklemek | Yeni yardımcı fonksiyon + tüm çağrı noktalarını bul | Policy builder’a koşul ekle |
| Policy görünürlüğü | İzin matrisi + dağınık yardımcılar | Hepsi policy builder’da |
| Koşulların tip güvenliği | Yok (yardımcılar zorunludur) | Kaynağa tipli jenerik koşullar |
| Servis metodu değişiklikleri | Bir can() + birden çok if bloğu | Bir 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-onlyyok) 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
| Kriter | RBAC | ABAC (Uygulama Seviyesi) | Harici Engine |
|---|---|---|---|
| İzinler bağımlı | Yalnızca rol | Rol + nitelikler | Rol + nitelikler + ilişkiler |
| Bağlamsal kural sayısı | 0 | 3-20 | 20+ veya servisler arası |
| Policy’ler nerede | Kod (TypeScript map) | Kod (builder/koşullar) | Harici policy deposu |
| Değerlendirme yeri | İşlem içi, senkron | İşlem içi, senkron | Ağ çağrısı, asenkron |
| Tip güvenliği | Tam (TypeScript) | Tam (TypeScript jenerikler) | Kısmi (policy dili) |
| Kontrol başına gecikme | Mikrosaniye | Mikrosaniye | Milisaniye (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
- NIST SP 800-162: Guide to Attribute Based Access Control (ABAC) - Dört nitelik kategorisi, fonksiyonel bileşenler ve ABAC dağıtımı için temel NIST standardı
- NIST SP 800-162 Tam Metin (PDF) - Özne, nesne, eylem ve ortam niteliklerinin resmi tanımlarını içeren ABAC rehberinin tam metni
- RBAC vs ABAC vs ReBAC (Web Dev Simplified) - ABAC koşul değerlendirmesi için kod örnekleri ile üç izin modelinin pratik TypeScript karşılaştırması
- OWASP Authorization Cheat Sheet - Merkezileştirilmiş yetkilendirme, varsayılan olarak reddetme ve ince taneli erişim kontrolü için ABAC önerileri dahil en iyi uygulamalar
- Attribute-Based Access Control (Wikipedia) - ABAC tarihçesi, XACML standardı ve PEP/PDP/PIP/PAP mimarisine genel bakış
- XACML - eXtensible Access Control Markup Language (Wikipedia) - Bu yazıda uygulama seviyesi eşdeğerlerine eşlenen XACML mimari bileşenleri için referans
- CASL - Isomorphic Authorization JavaScript Library -
can()fonksiyon deseni ve builder benzeri izin tanım API’sini ilham veren kütüphane - RBAC vs ABAC: Differences and When to Use (Oso) - Her modelin ne zaman uygun olduğunu ve hibrit yaklaşımları kapsayan detaylı karşılaştırma
- How to Implement ABAC Authorization (Permit.io) - Nitelik kategorileri ve PEP/PDP değerlendirme akışını kapsayan adım adım ABAC uygulama rehberi
- Using ABAC to Solve Role Explosion (Thoughtworks) - Geçiş desenleri ile ABAC’ın rol patlaması sorununu nasıl çözdüğünün analizi
- TypeScript Generics Documentation -
Condition<R extends Resource>deseni için kullanılan jenerik kısıtlama kalıpları için resmi referans - Cedar Policy Language (StrongDM) - Ödünleşimler bölümünde tartışılan harici policy engine alternatiflerine bağlam olarak AWS Cedar’a genel bakış
Ölçeklenebilir İzin Sistemleri
TypeScript ve Next.js ile ölçeklenebilir izin sistemleri oluşturma rehberi. Basit kontrol mekanizmalarından RBAC ve ABAC'a, oradan multi-tenant yetkilendirme sistemlerine kadar kapsamlı bir seri.
Serideki tüm yazılar
İlgili yazılar
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrarlanan izin mantığını ortadan kaldıran otomatik veritabanı sorgusu filtreleme ile genişletin.
İ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.
Authentication ve authorization farkı, yaygın izin sistemi tuzakları, fail-closed prensibi ve her izin sisteminin karşılaması gereken hedefler.
Dağınık izin kontrollerini merkezi bir service layer'a taşıyın, Next.js middleware guard'ları ekleyin ve derinlemesine savunma yetkilendirme mimarisi oluşturun.
TypeScript ile type-safe bir RBAC sistemi oluşturun, birleşik bir can() fonksiyonu yazın, UI ve backend'de izinleri senkronize edin ve RBAC'ın sınırlarını anlayın.