2026-03-15
İleri ABAC: Field-Level İzinler ve Veritabanı Entegrasyonu
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.
Özet
Post 104 builder pattern ile type-safe bir ABAC policy engine oluşturdu. can(user, action, resource, data?) fonksiyonu özne, kaynak ve eylem niteliklerini deklaratif koşullar aracılığıyla değerlendiriyor. Sahiplik, departman kapsamı ve kaynak durumu artık dağınık yardımcı fonksiyonlar değil, policy kuralları.
İki boşluk kaldı. Birincisi, can() bir boolean döndürüyor; kullanıcı ya kaynağın tamamını görüyor ya da hiçbir şeyi. Bir kullanıcının hangi alanları okuyup yazabileceğini kontrol etmenin yolu yok. Bir admin internalNotes alanını görüyor; bir yazar görmemeli. Bir editör content alanını güncelleyebiliyor ama publishedAt alanını güncelleyememeli. İkincisi, tüm koşullar zaten yüklenmiş nesneler üzerinde uygulama belleğinde değerlendiriliyor. Liste görünümleri için uygulama tüm kayıtları yüklüyor, her biri üzerinde can() çağırıyor ve başarısız olanları atıyor. Bu filtrelemeyi veritabanı yapmalı.
NIST SP 800-162 modeli ayrıca dördüncü bir nitelik kategorisi tanımlıyor: ortam. Post 104 bunu tanıttı ama ileriye dönük bir referans olarak bıraktı. Zaman bazlı erişim, IP kısıtlamaları ve feature flag’ler ayrı middleware kontrolleri olarak değil, policy engine içinde olmalı.
Bu yazı üç boşluğu da kapatıyor: ortam koşulları tip sistemine giriyor, alan seviyesinde izinler rol başına okuma ve yazma görünürlüğünü kontrol ediyor ve ABAC koşulları sorgu seviyesinde uygulama için veritabanı where cümlelerine dönüştürülüyor.
// Önizleme: tamamlanmış sistem nasıl görünüyor
const fields = getVisibleFields(session, 'document', docData);
const documents = await db.document.findMany({
where: toPrismaWhere(toWhereClause(session, 'document', 'read')),
});
Ortam Bazlı Kurallar
Tip Sisteminin Genişletilmesi
Post 104’ün Condition<R> tipi (user: User, data: ResourceDataMap[R]) => boolean alıyor. Ortam nitelikleri (zaman, IP adresi, yerel ayar, feature flag’ler) hem özne hem de kaynak dışında kalıyor. Kendi tiplerine ihtiyaçları var.
// lib/permissions.ts
interface Environment {
currentTime: Date;
ipAddress?: string;
locale?: string;
featureFlags?: Record<string, boolean>;
}
// Koşulu ortam parametresiyle genişlet
type Condition<R extends Resource> = (
user: User,
data: ResourceDataMap[R],
env?: Environment
) => boolean;
// Güncellenmiş can() imzası
function can<R extends Resource>(
user: User,
action: Action,
resource: R,
data?: ResourceDataMap[R],
env?: Environment
): boolean
can() imzası seri boyunca evrildi:
// Post 103 (RBAC):
can(role, resource, action): boolean
// Post 104 (ABAC):
can<R>(user, action, resource, data?): boolean
// Post 105 (İleri ABAC):
can<R>(user, action, resource, data?, env?): boolean
env parametresi opsiyonel. Post 104’ten mevcut koşullar değişmeden çalışmaya devam ediyor. Ortamı opsiyonel yapmak geriye dönük uyumluluğu korurken NIST dört-nitelik modelini tamamlıyor.
Pratik Örnekler
Zaman bazlı kısıtlama: fatura işlemleri yalnızca mesai saatlerinde:
.role('billing_admin')
.can(['create', 'update'], 'invoice', [
(user, data, env) => {
if (!env?.currentTime) return true; // ortam yok = zaman kısıtlaması yok
const hour = env.currentTime.getHours();
const day = env.currentTime.getDay();
return hour >= 9 && hour < 17 && day >= 1 && day <= 5;
},
])
IP bazlı kısıtlama: admin işlemleri ofis ağıyla sınırlı:
.role('admin')
.can('delete', 'document', [
(user, data, env) => {
if (!env?.ipAddress) return false; // IP bağlamı yoksa reddet
return env.ipAddress.startsWith('10.0.');
},
])
Feature flag geçişi: yeni işlevsellik feature flag’lerin arkasında:
.role('editor')
.can('publish', 'document', [
(user, doc) => user.departmentId === doc.departmentId,
(user, doc, env) => env?.featureFlags?.['bulk-publish'] === true,
])
Note: Ortam koşulları builder’daki diğer koşullarla aynı AND mantığını takip eder. İznin verilmesi için dizideki tüm koşulların geçmesi gerekir.
Service Layer Entegrasyonu
Service layer, Environment nesnesini istek bağlamından oluşturuyor. İstek başına bir kez oluşturur, sonra aktarır:
function getEnvironment(request: Request): Environment {
return {
currentTime: new Date(),
ipAddress: request.headers.get('x-forwarded-for') ?? undefined,
locale: request.headers.get('accept-language')?.split(',')[0],
featureFlags: getFeatureFlags(),
};
}
// Bir service metodu içinde
const env = getEnvironment(request);
if (!can(session, 'update', 'document', documentData, env)) {
throw new ForbiddenError();
}
Önemli bir tasarım kararı: ortam verileri istekten gelir, veritabanından değil. ResourceDataMap’in parçası olmamalı. İstek bağlamını kaynak verisiyle karıştırmak NIST modelini bozar ve koşulları anlamayı zorlaştırır.
Alan Seviyesinde Okuma İzinleri
Problem
Şu alanlara sahip bir document kaynağını düşünün: id, title, content, status, authorId, departmentId, internalNotes, reviewComments, publishedAt.
Farklı roller farklı alan görünürlüğüne ihtiyaç duyar:
| Alan | admin | editor | author (kendi) | viewer |
|---|---|---|---|---|
| id, title, content, status, publishedAt | evet | evet | evet | evet |
| authorId | evet | evet | evet | — |
| departmentId | evet | evet | — | — |
| internalNotes | evet | — | — | — |
| reviewComments | evet | evet (review’da) | — | — |
Alan seviyesinde izinler olmadan servis tam nesneyi döndürür ve frontend’in alanları gizlemesine güvenir. Bu belirsizliğe dayalı güvenlik. API yanıtı, UI’ın ne render ettiğinden bağımsız olarak hassas veri içerir.
Tip Sistemi Genişletmesi
Alan izin sistemi kaynakları alan adlarıyla ve koşullarla eşler:
type ResourceField<R extends Resource> = keyof ResourceDataMap[R] & string;
interface FieldPermissionEntry<R extends Resource> {
resource: R;
action: 'read' | 'write';
fields: ResourceField<R>[];
conditions?: Condition<R>[];
}
Builder’a alan izinlerini ekle:
.role('author')
.canReadFields('document',
['id', 'title', 'content', 'status', 'publishedAt', 'authorId'],
[(user, doc) => doc.authorId === user.userId]
)
.canReadFields('document',
['id', 'title', 'content', 'status', 'publishedAt']
// koşul yok -- herhangi bir belge için herkese açık alanlar
)
.role('editor')
.canReadFields('document',
['id', 'title', 'content', 'status', 'publishedAt', 'authorId', 'departmentId']
)
.canReadFields('document',
['id', 'title', 'content', 'status', 'publishedAt', 'authorId', 'departmentId', 'reviewComments'],
[(user, doc) => doc.status === 'review']
)
// Admin: canReadFields girişi yok = tüm alanlar görünür (konvansiyon)
Konvansiyon: canReadFields girişlerinin yokluğu, rolün can() aracılığıyla read erişimi olması koşuluyla o rol için tüm alanların görünür olduğu anlamına gelir.
getVisibleFields() Fonksiyonu
function getVisibleFields<R extends Resource>(
user: User,
resource: R,
data?: ResourceDataMap[R],
env?: Environment
): ResourceField<R>[] {
// 1. Kullanıcının okuma erişimi var mı kontrol et
if (!can(user, 'read', resource, data, env)) return [];
// 2. Bu rol + kaynak + 'read' için alan izin girişlerini bul
const fieldEntries = fieldPermissions[user.role]
?.filter(e => e.resource === resource && e.action === 'read') ?? [];
// 3. Alan girişi yoksa tüm alanları döndür (kısıtlama yok)
if (fieldEntries.length === 0) {
return Object.keys(resourceSchemas[resource]) as ResourceField<R>[];
}
// 4. Eşleşen girişlerden alanları topla (birleşim)
const visibleFields = new Set<ResourceField<R>>();
for (const entry of fieldEntries) {
if (!entry.conditions || entry.conditions.length === 0) {
entry.fields.forEach(f => visibleFields.add(f));
} else if (data) {
const conditionsMet = entry.conditions.every(c => c(user, data, env));
if (conditionsMet) {
entry.fields.forEach(f => visibleFields.add(f));
}
}
}
return Array.from(visibleFields);
}
filterFields() yardımcı fonksiyonu bir kaynak nesnesi ve görünür alanlar listesi alır, yalnızca o alanları içeren yeni bir nesne döndürür:
function filterFields<R extends Resource>(
data: ResourceDataMap[R],
fields: ResourceField<R>[]
): Partial<ResourceDataMap[R]> {
const result: Partial<ResourceDataMap[R]> = {};
for (const field of fields) {
if (field in data) {
(result as Record<string, unknown>)[field] = data[field as keyof typeof data];
}
}
return result;
}
Service Layer Entegrasyonu
export async function getDocument(documentId: string) {
const session = await requireSession();
const document = await db.document.findUnique({ where: { id: documentId } });
if (!document) throw new NotFoundError();
const docData = toResourceData(document);
if (!can(session, 'read', 'document', docData)) {
throw new ForbiddenError();
}
// Rol ve koşullara göre alanları filtrele
const visibleFields = getVisibleFields(session, 'document', docData);
return filterFields(docData, visibleFields);
}
UI’da Alan Gizleme
İstemci tarafında, alanları koşullu olarak render etmek için getVisibleFields() kullanın:
function DocumentDetail({ document, session }: Props) {
const fields = getVisibleFields(session, 'document', document);
return (
<div>
<h1>{document.title}</h1>
{fields.includes('internalNotes') && (
<section>{document.internalNotes}</section>
)}
{fields.includes('reviewComments') && (
<section>{document.reviewComments}</section>
)}
</div>
);
}
UI gizleme UX içindir, güvenlik için değil. Service layer API yanıtındaki alanları zaten filtreledi. İstemci almadığı şeyi render edemez. Post 102’de belirlendiği gibi: sunucu güvenlik sınırıdır, istemci UX kolaylığıdır.
Alan Seviyesinde Yazma İzinleri
Builder Genişletmesi
Yazma izinleri okuma izinlerinden farklıdır. Bir editör internalNotes alanını okuyabilir ama yazamayabilir. Bir moderatör status yazabilir ama content yazamayabilir.
.role('author')
.canWriteFields('document', ['title', 'content'])
.role('editor')
.canWriteFields('document', ['title', 'content', 'status'], [
(user, doc) => user.departmentId === doc.departmentId,
])
.role('admin')
// canWriteFields yok = tüm alanlara yazabilir (update erişimi varsa)
Yazma izin matrisi:
| Alan | admin | editor (dept) | author (kendi) |
|---|---|---|---|
| title | evet | evet | evet |
| content | evet | evet | evet |
| status | evet | evet | — |
| internalNotes | evet | — | — |
| reviewComments | evet | evet | — |
| publishedAt | evet | — | — |
pickPermittedFields() Fonksiyonu
function pickPermittedFields<R extends Resource>(
user: User,
resource: R,
input: Partial<ResourceDataMap[R]>,
data?: ResourceDataMap[R], // koşullar için mevcut kaynak verisi
env?: Environment
): Partial<ResourceDataMap[R]> {
// 1. Bu rol + kaynak için yazma alan girişlerini bul
const fieldEntries = fieldPermissions[user.role]
?.filter(e => e.resource === resource && e.action === 'write') ?? [];
// 2. Giriş yoksa tüm alanlara izin var (admin durumu)
if (fieldEntries.length === 0) return input;
// 3. İzin verilen yazma alanlarını topla
const permitted = new Set<string>();
for (const entry of fieldEntries) {
if (!entry.conditions || entry.conditions.length === 0) {
entry.fields.forEach(f => permitted.add(f));
} else if (data) {
const conditionsMet = entry.conditions.every(c => c(user, data, env));
if (conditionsMet) entry.fields.forEach(f => permitted.add(f));
}
}
// 4. Girdiyi yalnızca izin verilen alanlara filtrele
const result: Partial<ResourceDataMap[R]> = {};
for (const [key, value] of Object.entries(input)) {
if (permitted.has(key)) {
(result as Record<string, unknown>)[key] = value;
}
}
return result;
}
Sessiz Düşürme vs. Hata
Bir kullanıcı yasaklanmış bir alan gönderdiğinde iki yaklaşım:
- Sessiz düşürme: Alanı çıkar ve devam et. Kullanıcı alanın yok sayıldığını bilmez. İstemci için daha basit, ama hataları gizler.
- Hata: Tüm gönderimi 403 ile reddet. Daha açık, ama istemcinin göndermeden önce hangi alanlara izin verildiğini bilmesini gerektirir.
// Seçenek A: Sessiz düşürme (varsayılan, CASL bu yaklaşımı kullanır)
const permitted = pickPermittedFields(session, 'document', input, docData);
await db.document.update({ where: { id }, data: permitted });
// Seçenek B: Açık doğrulama (admin/denetim bağlamları için)
function validatePermittedFields<R extends Resource>(
user: User,
resource: R,
input: Partial<ResourceDataMap[R]>,
data?: ResourceDataMap[R],
env?: Environment
): void {
const permitted = pickPermittedFields(user, resource, input, data, env);
const forbidden = Object.keys(input).filter(k => !(k in permitted));
if (forbidden.length > 0) {
throw new ForbiddenError(`Bu alanlara yazılamaz: ${forbidden.join(', ')}`);
}
}
Deneyimlerime göre, API’ler için sessiz düşürme, admin bağlamları için hata iyi çalışıyor. Service layer, işlemin hassasiyetine göre hangisini kullanacağını seçer.
Create ve Update Uygulaması
Create Akışı
Create işleminde mevcut kaynak verisi yoktur. Kaynak niteliklerine referans veren koşullar (sahiplik, departman) değerlendirilemez. Create için alan izinleri koşulsuz girişler kullanmalıdır:
export async function createDocument(input: DocumentInput) {
const session = await requireSession();
if (!can(session, 'create', 'document')) {
throw new ForbiddenError();
}
// Girdiyi yalnızca bu rolün yazabileceği alanlara filtrele
const permitted = pickPermittedFields(session, 'document', input);
// Sistem authorId ve status'u ayarlar -- kullanıcı girdisinden değil
return await db.document.create({
data: {
...permitted,
authorId: session.userId,
status: 'draft',
},
});
}
authorId ve status sistem tarafından yönetilen alanlardır, asla kullanıcı girdisinden gelmez. İstemci authorId gönderse bile pickPermittedFields() onu çıkarır çünkü yazarın yazma alanları arasında değildir.
Update Akışı
Update işleminde mevcut kaynak verisi koşullar için kullanılabilir:
export async function updateDocument(documentId: string, input: DocumentInput) {
const session = await requireSession();
const document = await db.document.findUnique({ where: { id: documentId } });
if (!document) throw new NotFoundError();
const docData = toResourceData(document);
if (!can(session, 'update', 'document', docData)) {
throw new ForbiddenError();
}
// Girdiyi yalnızca bu kullanıcının yazabileceği alanlara filtrele
const permitted = pickPermittedFields(session, 'document', input, docData);
return await db.document.update({
where: { id: documentId },
data: permitted,
});
}
Koşullu Form Render
UI, form alanlarını koşullu olarak render etmek için yazma alan izinlerini kullanabilir:
function DocumentForm({ document, session }: Props) {
const writeFields = getWritableFields(session, 'document', document);
return (
<form>
{writeFields.includes('title') && (
<input name="title" defaultValue={document?.title} />
)}
{writeFields.includes('content') && (
<textarea name="content" defaultValue={document?.content} />
)}
{writeFields.includes('status') && (
<select name="status" defaultValue={document?.status}>
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
)}
{writeFields.includes('internalNotes') && (
<textarea name="internalNotes" defaultValue={document?.internalNotes} />
)}
</form>
);
}
Form render’ı UX’tir. Sunucu tarafındaki pickPermittedFields() güvenlik sınırıdır. Kötü niyetli bir istemci form gönderisine gizli alanlar eklese bile pickPermittedFields() bunları çıkarır.
Otomatik Veritabanı Sorgusu Filtreleme
ConditionDescriptor Yaklaşımı
Post 104’ün koşulları opak fonksiyonlardır. Bellekte değerlendirilir ama SQL’e çevrilemez. Liste görünümleri için (“okuyabileceğim tüm belgeleri göster”) tüm kayıtları yükleyip can() ile bir döngüde filtrelemek israf.
Fikir: her koşul fonksiyonunun yanında, veritabanı terimleriyle ne yaptığını açıklayan bir deklaratif tanımlayıcı sağla:
interface ConditionDescriptor<R extends Resource> {
// Bellek içi değerlendirme fonksiyonu (eskisiyle aynı)
evaluate: (user: User, data: ResourceDataMap[R], env?: Environment) => boolean;
// Veritabanı çevirisi için deklaratif tanım
toFilter?: (user: User, env?: Environment) => WhereClause<R> | null;
}
// ORM-bağımsız where cümle temsili
type WhereClause<R extends Resource> = {
[K in keyof ResourceDataMap[R]]?:
| ResourceDataMap[R][K] // eşitlik
| { $ne: ResourceDataMap[R][K] } // eşit değil
| { $in: ResourceDataMap[R][K][] } // listede
| { $gte: ResourceDataMap[R][K] } // büyük veya eşit
| { $lte: ResourceDataMap[R][K] } // küçük veya eşit
};
Bu sözdizimi MongoDB’nin sorgu formatına ve CASL’ın koşullarına benzer. Prisma, Drizzle ve diğer ORM’ler bunu basit bir adaptörle tüketebilir.
Tanımlayıcılarla güncellenmiş builder:
const permissions = new PermissionBuilder()
.role('editor')
.can(['create', 'read', 'update', 'publish'], 'document', [{
evaluate: (user, doc) => user.departmentId === doc.departmentId,
toFilter: (user) => ({ departmentId: user.departmentId }),
}])
.role('author')
.can(['read', 'update'], 'document', [{
evaluate: (user, doc) => doc.authorId === user.userId,
toFilter: (user) => ({ authorId: user.userId }),
}])
.role('admin')
.can(['create', 'read', 'update', 'delete', 'publish'], 'document')
// Koşul yok = filtre yok = tüm kayıtlar
.role('viewer')
.can('read', 'document')
// Koşul yok = filtre yok = tüm yayınlanmış kayıtlar
.build();
toWhereClause() Fonksiyonu
function toWhereClause<R extends Resource>(
user: User,
resource: R,
action: Action,
env?: Environment
): WhereClause<R> | null {
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şulsuz giriş = filtre gerekmiyor
if (!entry.conditions || entry.conditions.length === 0) {
return {}; // boş where = tüm kayıtlar
}
// Çevrilebilir koşullardan filtreleri topla
const filters: WhereClause<R> = {};
let allTranslatable = true;
for (const condition of entry.conditions) {
if (condition.toFilter) {
const filter = condition.toFilter(user, env);
if (filter) Object.assign(filters, filter);
} else {
allTranslatable = false;
}
}
if (allTranslatable) return filters;
// Bazı koşullar çevrilemiyorsa null döndür (bellek içi filtrelemeye geri dön)
return null;
}
return null; // eşleşen giriş yok = reddet
}
Warning: Boş nesne
{}venullfarklı anlamlara sahiptir.{}“filtre yok; tüm kayıtları döndür” anlamına gelir (admin/viewer durumu).null“eşleşen izin yok; erişimi reddet” anlamına gelir. Reddedilen bir rol içinnullyerine{}döndürmek tüm kayıtları döndürür. Bu ayrım güvenlik açısından kritiktir.
ORM Adaptör Katmanı
WhereClause<R> ORM-bağımsızdır. Adaptörler bunu ORM’ye özel sözdizimine dönüştürür:
// Prisma adaptörü
function toPrismaWhere<R extends Resource>(
clause: WhereClause<R>
): Record<string, unknown> {
const prismaWhere: Record<string, unknown> = {};
for (const [key, value] of Object.entries(clause)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const op = value as Record<string, unknown>;
if ('$ne' in op) prismaWhere[key] = { not: op.$ne };
else if ('$in' in op) prismaWhere[key] = { in: op.$in };
else if ('$gte' in op) prismaWhere[key] = { gte: op.$gte };
else if ('$lte' in op) prismaWhere[key] = { lte: op.$lte };
} else {
prismaWhere[key] = value; // eşitlik
}
}
return prismaWhere;
}
// Drizzle adaptörü
function toDrizzleWhere<R extends Resource>(
clause: WhereClause<R>,
table: Record<string, Column>
): SQL[] {
const conditions: SQL[] = [];
for (const [key, value] of Object.entries(clause)) {
if (typeof value === 'object' && value !== null) {
const op = value as Record<string, unknown>;
if ('$ne' in op) conditions.push(ne(table[key], op.$ne));
else if ('$in' in op) conditions.push(inArray(table[key], op.$in as unknown[]));
else if ('$gte' in op) conditions.push(gte(table[key], op.$gte));
else if ('$lte' in op) conditions.push(lte(table[key], op.$lte));
} else {
conditions.push(eq(table[key], value));
}
}
return conditions;
}
Liste görünümleri için tam service layer entegrasyonu:
export async function listDocuments() {
const session = await requireSession();
const whereClause = toWhereClause(session, 'document', 'read');
if (whereClause === null) {
// null = reddet veya çevrilemeyen koşullar -> boş döndür
return [];
}
// İzinleri sorguya aktar
const documents = await db.document.findMany({
where: toPrismaWhere(whereClause),
});
// Her belgeye alan seviyesinde filtreleme uygula
return documents.map(doc => {
const docData = toResourceData(doc);
const fields = getVisibleFields(session, 'document', docData);
return filterFields(docData, fields);
});
}
Veritabanına Aktarılamayan Koşullar
Tüm koşullar çevrilebilir değildir. Karmaşık mantık, kaynaklar arası koşullar ve ortam bazlı kontroller genellikle bellekte kalır:
(user, doc, env) => env.currentTime.getHours() >= 9: çalışma zamanı bağlamını içerir, kaynak niteliği değil(user, doc) => doc.tags.some(t => user.expertise.includes(t)): dizi kesişim mantığı(user, doc) => doc.wordCount > 1000 && user.role === 'senior_editor': özne ve kaynağı karıştıran bileşik mantık
toFilter alanı opsiyoneldir. Belirtilmezse koşul bellek içi değerlendirmeye geri döner. Sistem nazikçe degrade olur: çevrilebilir koşullar WHERE cümlelerine dönüşür, çevrilemeyenler post-fetch filtreleme gerektirir.
Bu, üretim yetkilendirme sistemleri tarafından kullanılan aynı kalıptır. OPA’nın Compile API’si buna “kısmi değerlendirme” diyor. Cerbos’un PlanResources API’si üç sonuç döndürüyor: ALWAYS_ALLOWED, ALWAYS_DENIED veya bir AST ile CONDITIONAL. Buradaki hafif TypeScript sürümü daha az altyapıyla aynı prensibi takip ediyor.
Birleşik Sistem
Tamamlanan sistem policy builder’ı tek doğruluk kaynağı yapıyor. Builder’daki bir koşulu değiştirmek otomatik olarak her katmana yayılır:
can(): kayıt seviyesinde erişim kontrolügetVisibleFields(): alan seviyesinde okuma izinleripickPermittedFields(): alan seviyesinde yazma izinleritoWhereClause(): veritabanı sorgusu filtreleme
Hiçbir service metodu, React bileşeni veya veritabanı sorgusunun değişmesi gerekmez.
Örnek: “Editörler artık belge review durumundaysa reviewComments alanını da görebilir.” Builder’da tek bir değişiklik:
.role('editor')
.canReadFields('document',
['id', 'title', 'content', 'status', 'reviewComments', 'publishedAt'],
[(user, doc) => doc.status === 'review']
)
getVisibleFields()artıkstatus === 'review'olduğunda editörler içinreviewCommentsdöndürüyor- React bileşeni zaten
{fields.includes('reviewComments') && ...}içeriyor; otomatik olarak render ediyor - API yanıtı zaten
filterFields()kullanıyor; alanı otomatik olarak dahil ediyor - Service metodu değişikliği yok. Bileşen değişikliği yok. Veritabanı sorgusu değişikliği yok.
ABAC Artıları ve Eksileri
ABAC’ın Öne Çıktığı Durumlar
- Kaynak başına 3+ bağlamsal kural: İzinler sahiplik, departman, durum, zaman ve diğer niteliklere bağlıysa, ABAC Post 103’teki yardımcı fonksiyon çoğalmasını ortadan kaldırır.
- Alan seviyesinde görünürlük gereksinimleri: Farklı roller aynı kaynağın farklı alanlarını gördüğünde, alan izinleri ad-hoc alan çıkarma işleminden daha temizdir.
- Veritabanı seviyesinde uygulama gerekli: Liste görünümleri verimli olmalıysa,
toWhereClause()kalıbı bellek içi filtrelemeyi ortadan kaldırır. - Denetim gereksinimleri: Merkezi bir policy builder, dağınık yardımcı fonksiyonlardan denetlemesi daha kolaydır. “Bir editör ne yapabilir?” sorusu builder’ın bir bölümünü okuyarak cevaplanabilir.
- Policy değişiklikleri sık: İş kuralları sık değiştiğinde, builder’daki tek bir koşulu değiştirmek tüm service metotlarını ve bileşenleri güncellemekten daha hızlı ve güvenlidir.
ABAC’ın Gereksiz Olduğu Durumlar
- Basit rol bazlı erişim: İzinler yalnızca role bağlıysa ve bağlamsal koşullar yoksa, Post 103’ten RBAC daha basit ve eşit derecede doğrudur.
- Küçük ekip, az kaynak: 2-3 kaynak ve 3-4 rolle, ABAC tip sistemi ek yükü (generic’ler, builder’lar, koşul tanımlayıcıları) kurtardığı karmaşıklığı aşabilir.
- Alan seviyesinde gereksinim yok: Tüm kullanıcılar bir kaynağın tüm alanlarını görüyorsa, alan izin katmanı fayda sağlamadan karmaşıklık ekler.
- Prototipleme aşaması: ABAC’ın tip sistemi yeniden düzenlemeyi zorlaştırır. Kaynak şekillerinin sık değiştiği hızlı prototipleme sırasında daha basit kontroller daha pratiktir.
Tip: RBAC (Post 103) ile başla. Yardımcı fonksiyonlar çoğalmaya başladığında ABAC koşullarını ekle. Farklı roller farklı alan görünürlüğüne ihtiyaç duyduğunda alan seviyesinde izinleri ekle. Liste görünümleri çok fazla kayıt yüklediğinde DB sorgusu filtrelemeyi ekle.
Karar Çerçevesi
| Katman | Ne Zaman Ekle… | Ne Zaman Atla… |
|---|---|---|
| Ortam kuralları | Zaman/IP/yerel ayar koşulları var; uyumluluk bağlam duyarlı erişim gerektiriyor | Tüm izinler bağlamdan bağımsız |
| Alan seviyesinde okuma | Farklı roller farklı alanlar görüyor; hassas veri var | Tüm roller tüm alanları görüyor |
| Alan seviyesinde yazma | Kullanıcılar yalnızca belirli alanları değiştirebiliyor; formlar role göre değişiyor | Tüm yazan kullanıcılar tüm alanları değiştirebiliyor |
| DB sorgusu filtreleme | Kısıtlı rollerle liste görünümleri; büyük veri setleri | Yalnızca tekil kayıt görünümleri; küçük veri setleri |
Yaygın Tuzaklar
- İç içe kaynaklarda alan filtrelemeyi unutmak: Bir belgenin
projectilişkisi varsa,document.projectyüklemek proje alan izinlerini atlar. İç içe kaynaklara dafilterFields()uygulayın. - Koşul tanımlayıcı sapması:
evaluatefonksiyonu vetoFiltertanımlayıcısı eşdeğer sonuçlar üretmelidir. Sapmayı yakalamak için her ikisini de aynı doğruluk tablosuna karşı test edin. - Alan seviyesinde izinleri aşırı kullanmak: Her kaynak alan seviyesinde kontrole ihtiyaç duymaz. Varsayılan (alan girişi yok = tüm alanlar görünür) kısıtlaması olmayan kaynaklar için işleri basit tutar.
nullve{}karıştırmak:toWhereClause()’da{}“filtre yok” (tüm kayıtlar) venull“erişim yok” (reddet) anlamına gelir. Bunu yanlış yapmak güvenlik açığıdır.- DB sorgularında
selecteksikliği:toWhereClause()satırları filtreler, sütunları değil. Gerçek DB seviyesinde alan uygulaması için bunu sütun seçimiyle birleştirin. Pratikte uygulama seviyesindefilterFields()genellikle yeterlidir.
Sırada Ne Var
İzin sistemi artık kayıt seviyesinde erişimi (can()), alan seviyesinde görünürlüğü (getVisibleFields(), pickPermittedFields()) ve veritabanı seviyesinde uygulamayı (toWhereClause()) kapsıyor. Ortam koşulları NIST dört-nitelik modelini tamamlıyor.
Post 106 kalan üretim endişelerini ele alıyor: çok kiracılık (kiracı izolasyonunu birinci sınıf bir izin kavramı olarak), izin kütüphanesi değerlendirmesi (CASL, Oso, Cerbos, Cedar; ne zaman kütüphane, ne zaman özel kod kullanılmalı) ve ekip boyutu, düzenleyici gereksinimler ve sistem karmaşıklığına göre doğru yetkilendirme yaklaşımını seçmek için son mimari karar çerçevesi.
Kaynaklar
- NIST SP 800-162: Guide to Attribute Based Access Control (ABAC) - Özne, kaynak ve eylem yanında ortam niteliklerini dördüncü ABAC kategorisi olarak tanımlayan temel NIST standardı
- CASL v6 - Restricting Fields Access -
permittedFieldsOf()ve kural tanımlarındaki alan dizileri kullanarak alan seviyesinde kısıtlamalar hakkında resmi CASL dokümantasyonu - CASL - Isomorphic Authorization JavaScript Library - JavaScript/TypeScript’te
can()+ alan kısıtlama kalıbına öncülük eden ve MongoDB tarzı koşullarla veritabanı sorgularına eşleme yapan kütüphane - Cerbos - Filtering Database Results with Query Plans - Cerbos PlanResources API’sinin adaptörlerin Prisma, MongoDB veya SQL sorgularına dönüştürdüğü bir AST döndürmesi
- Cerbos Prisma Integration V2.0 - Yetkilendirme sorgu planlarını iç içe alan desteğiyle Prisma
wherecümlelerine dönüştürme - OPA - Write Policy in OPA, Enforce Policy in SQL - Rego policy’lerini SQL WHERE cümlelerine derleyen OPA kısmi değerlendirme kalıbı
- Permit.io - Why Data Filtering Matters for Database Authorization - OPA, Cerbos ve SpiceDB genelinde kaynak seviyesi ve geçit seviyesi veri filtreleme kalıplarının analizi
- Drizzle ORM - Dynamic Query Building - Birleştirilebilir sorgu oluşturma için
$dynamic()hakkında resmi Drizzle dokümantasyonu - RBAC vs ABAC: Differences and When to Use (Oso) - ABAC karmaşıklığının ne zaman haklı olduğu ve RBAC’ın ne zaman yeterli olduğuna dair karar kriterleri
- ZenStack - Three Ways to Secure Database APIs - Veritabanı seviyesi (RLS), ORM seviyesi ve uygulama seviyesi yetkilendirme stratejilerinin karşılaştırması
- TypeScript Generics Documentation - ABAC tip sistemi boyunca kullanılan generic kısıtlama kalıpları için resmi referans
- OWASP Authorization Cheat Sheet - Merkezi yetkilendirme, varsayılan olarak reddet ve ayrıntılı erişim kontrolü önerileri dahil en iyi uygulamalar
Ö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
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.
İ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.