Skip to content
~/sph.sh

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

typescript
// Önizleme: tamamlanmış sistem nasıl görünüyorconst 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.

typescript
// lib/permissions.ts
interface Environment {  currentTime: Date;  ipAddress?: string;  locale?: string;  featureFlags?: Record<string, boolean>;}
// Koşulu ortam parametresiyle genişlettype 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:

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

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

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

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

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

Alanadmineditorauthor (kendi)viewer
id, title, content, status, publishedAtevetevetevetevet
authorIdevetevetevet--
departmentIdevetevet----
internalNotesevet------
reviewCommentsevetevet (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:

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

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

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

typescript
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

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

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

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

Alanadmineditor (dept)author (kendi)
titleevetevetevet
contentevetevetevet
statusevetevet--
internalNotesevet----
reviewCommentsevetevet--
publishedAtevet----

pickPermittedFields() Fonksiyonu

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

  1. 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.
  2. Hata: Tüm gönderimi 403 ile reddet. Daha açık, ama istemcinin göndermeden önce hangi alanlara izin verildiğini bilmesini gerektirir.
typescript
// 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:

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

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

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

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

typescript
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

typescript
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 {} ve null farklı 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çin null yerine {} 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:

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

typescript
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 izinleri
  • pickPermittedFields(): alan seviyesinde yazma izinleri
  • toWhereClause(): 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:

typescript
.role('editor')  .canReadFields('document',    ['id', 'title', 'content', 'status', 'reviewComments', 'publishedAt'],    [(user, doc) => doc.status === 'review']  )
  • getVisibleFields() artık status === 'review' olduğunda editörler için reviewComments dö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

  1. 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.
  2. 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.
  3. Veritabanı seviyesinde uygulama gerekli: Liste görünümleri verimli olmalıysa, toWhereClause() kalıbı bellek içi filtrelemeyi ortadan kaldırır.
  4. 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.
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

KatmanNe Zaman Ekle...Ne Zaman Atla...
Ortam kurallarıZaman/IP/yerel ayar koşulları var; uyumluluk bağlam duyarlı erişim gerektiriyorTüm izinler bağlamdan bağımsız
Alan seviyesinde okumaFarklı roller farklı alanlar görüyor; hassas veri varTüm roller tüm alanları görüyor
Alan seviyesinde yazmaKullanıcılar yalnızca belirli alanları değiştirebiliyor; formlar role göre değişiyorTüm yazan kullanıcılar tüm alanları değiştirebiliyor
DB sorgusu filtrelemeKısıtlı rollerle liste görünümleri; büyük veri setleriYalnızca tekil kayıt görünümleri; küçük veri setleri

Yaygın Tuzaklar

  1. İç içe kaynaklarda alan filtrelemeyi unutmak: Bir belgenin project ilişkisi varsa, document.project yüklemek proje alan izinlerini atlar. İç içe kaynaklara da filterFields() uygulayın.
  2. Koşul tanımlayıcı sapması: evaluate fonksiyonu ve toFilter tanımlayıcısı eşdeğer sonuçlar üretmelidir. Sapmayı yakalamak için her ikisini de aynı doğruluk tablosuna karşı test edin.
  3. 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.
  4. null ve {} karıştırmak: toWhereClause()'da {} "filtre yok" (tüm kayıtlar) ve null "erişim yok" (reddet) anlamına gelir. Bunu yanlış yapmak güvenlik açığıdır.
  5. DB sorgularında select eksikliğ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 seviyesinde filterFields() 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

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

İlerleme5/6 yazı tamamlandı

İlgili Yazılar