Skip to content
~/sph.sh

RBAC: TypeScript ile Type-Safe Rol Bazlı Erişim Kontrolü

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.

Özet

Post 102 yetkilendirmeyi service layer'da merkezileştirdi; izin kontrollerinin nerede yapılacağı sorusunu çözdü. Ancak service layer içindeki izin kuralları hala sabit kodlanmış if/else zincirleri: if (session.role === 'admin') return true. Yeni bir rol eklemek, her servis dosyasındaki her yardımcı fonksiyonu değiştirmek anlamına geliyor.

Bu yazı o zincirleri RBAC (Rol Bazlı Erişim Kontrolü) ile değiştiriyor. Ferraiolo ve Kuhn tarafından 1992'de biçimlendirilen ve NIST tarafından INCITS 359-2004 olarak standartlaştırılan bir modeldir. Tek bir type-safe can() fonksiyonu tüm sabit kodlanmış kontrollerin yerini alıyor. Hem sunucu hem istemci tarafında çalışarak Post 102'nin bir sınırlama olarak belirttiği izin mantığı tekrarını ortadan kaldırıyor.

RBAC Nedir?

NIST Modeli

RBAC, izinleri doğrudan kullanıcılara değil rollere atar. Kullanıcılar, rollere atanarak izinleri edinir. Bu dolaylı bağlantı temel kavramdır: birisi görev değiştirdiğinde elli ayrı izni değil rolünü değiştirin.

NIST modeli üç temel bileşen tanımlar:

  • Kullanıcılar: Kimliği doğrulanmış kimlikler (bizim durumumuzda verifySession())
  • Roller: İsimlendirilmiş görev fonksiyonları: admin, editor, author, viewer
  • İzinler: Kaynaklar üzerinde onaylanmış operasyonlar: document:create, project:read

Roller olmadan, izin yönetimi N kullanıcı çarpı M izin ataması gerektirir. Rollerle çok daha küçük bir küme yönetirsiniz: N kullanıcı-rol ataması artı R rol-izin eşlemesi.

Bizim Domainimizde RBAC

NIST kavramlarını Post 101-102'deki domain'e eşleyelim:

NIST KavramıBizim Domainimiz
KullanıcıverifySession() ile doğrulanmış oturum
Roladmin, editor, author, viewer
Nesne (Kaynak)document, project
Operasyon (Eylem)create, read, update, delete, publish
İzinRol-(kaynak, eylem) eşlemesi

Kullanıcılar rollere bağlanır. Roller izinlere bağlanır. Hiçbir kullanıcı doğrudan bir izne bağlanmaz. Bu, temel RBAC yapısıdır.

TypeScript İzin Tanımları

Kaynaklar ve Eylemler

İlk adım, hangi kaynakların ve eylemlerin var olduğunu tanımlamaktır. TypeScript'in as const ifadesi, string[]'e genişletmek yerine literal türleri korur.

typescript
// lib/permissions.ts// 'server-only' yok -- bu dosya sunucu ve istemci arasında paylaşılıyor
export const RESOURCES = ['document', 'project'] as const;export type Resource = (typeof RESOURCES)[number];
export const ACTIONS = [  'create',  'read',  'update',  'delete',  'publish',] as const;export type Action = (typeof ACTIONS)[number];
// Template literal türü: "document:create" | "document:read" | ...export type Permission = `${Resource}:${Action}`;

Permission türü, tüm geçerli Resource:Action kombinasyonlarının bir birleşimidir. 'document:fly' yazmak derleme zamanı hatası olur. TypeScript bu birleşimi iki as const dizisinden otomatik olarak oluşturur.

Roller

typescript
export const ROLES = ['admin', 'editor', 'author', 'viewer'] as const;export type Role = (typeof ROLES)[number];

İzin Haritası

RBAC sisteminin çekirdeği budur: her rolün izinlerini tanımlayan tek bir nesne.

typescript
export const ROLE_PERMISSIONS = {  admin: [    'document:create',    'document:read',    'document:update',    'document:delete',    'document:publish',    'project:create',    'project:read',    'project:update',    'project:delete',  ],  editor: [    'document:create',    'document:read',    'document:update',    'document:publish',    'project:read',  ],  author: [    'document:create',    'document:read',    'document:update',    'project:read',  ],  viewer: [    'document:read',    'project:read',  ],} as const satisfies Record<Role, readonly Permission[]>;

Burada iki TypeScript özelliği birlikte çalışır:

  • as const literal türleri korur; her izin string değil 'document:create' olarak kalır
  • satisfies şekli genişletmeden doğrular. Bir rol yanlış yazılırsa veya bir izin dizgisi geçersizse (örneğin 'document:fly'), TypeScript bunu derleme zamanında yakalar. Ancak çıkarsanan tür yine de belirli literal değerleri koruyarak otomatik tamamlama sağlar

Neden tür anotasyonu yerine satisfies? const ROLE_PERMISSIONS: Record<Role, Permission[]> yazmak tüm değerleri Permission[]'e genişletir ve belirli literal bilgiyi kaybeder. satisfies ile TypeScript her rolün tam olarak hangi izinlere sahip olduğunu bilir.

İzin Matrisi

Yukarıdaki ROLE_PERMISSIONS nesnesi bir tablo olarak görselleştirilebilir. Bu tablo gözden geçirilebilir, denetlenebilir ve kod okumayı gerektirmez.

İzinadmineditorauthorviewer
document:createevetevetevet--
document:readevetevetevetevet
document:updateevetevetevet--
document:deleteevet------
document:publishevetevet----
project:createevet------
project:readevetevetevetevet
project:updateevet------
project:deleteevet------

Bu tablo izin sisteminin kendisidir. "Bir editör ne yapabilir?" sorusu tek bakışta cevaplanır.

can() Fonksiyonu

Uygulama

Tek bir fonksiyon, tüm sabit kodlanmış rol kontrollerinin yerini alır.

typescript
// lib/permissions.ts (devam)
export function can(  role: Role,  resource: Resource,  action: Action): boolean {  const permissions = ROLE_PERMISSIONS[role];  const required = `${resource}:${action}` as Permission;  return (permissions as readonly Permission[]).includes(required);}
// Alternatif: tam izin dizgisi ile kontrolexport function hasPermission(  role: Role,  permission: Permission): boolean {  return (ROLE_PERMISSIONS[role] as readonly Permission[]).includes(    permission  );}

Temel özellikler:

  • Saf fonksiyon: Veritabanı erişimi yok, async yok, yan etki yok. Sıfır ek yük.
  • Type-safe: TypeScript role'ü dört literal türden birine daraltır. Geçersiz kaynak veya eylem dizgileri derleme zamanı hatalarıdır.
  • İki form: can(role, 'document', 'update') servis metotlarında doğal okunur. hasPermission(role, 'document:update') UI bileşenlerinde doğal okunur. İkisi de aynı ROLE_PERMISSIONS haritasını sorgular.

Öncesi ve Sonrası: Service Layer Yeniden Düzenleme

Öncesi (Post 102'den sabit kodlanmış kontroller):

typescript
function canEditDocument(  session: { userId: string; role: string },  document: DocumentWithProject): boolean {  if (session.role === 'admin') return true;
  const membership = document.project.members.find(    (m) => m.userId === session.userId  );  if (!membership) return false;
  if (membership.role === 'editor') return true;
  if (    membership.role === 'author' &&    document.authorId === session.userId  ) {    return true;  }
  return false;}

Sonrası (can() ile RBAC):

typescript
function canEditDocument(  session: { userId: string; role: Role },  document: DocumentWithProject): boolean {  // Rol bazlı kontrol: rolün document:update izni var mı?  if (can(session.role, 'document', 'update')) return true;
  // Sahiplik kontrolü: yazarlar kendi belgelerini düzenleyebilir  // (RBAC'ın sınırına ulaştığımız yer -- Sınırlamalar bölümüne bakın)  const membership = document.project.members.find(    (m) => m.userId === session.userId  );  if (!membership) return false;
  if (    membership.role === 'author' &&    document.authorId === session.userId  ) {    return true;  }
  return false;}

Ne değişti:

  • if (session.role === 'admin') return true ifadesi if (can(session.role, 'document', 'update')) return true oldu
  • Admin kontrolü artık özel durum değil. admin rolünün izin haritasında document:update olduğu için çalışıyor.
  • Belgeleri düzenleyebilen bir "moderator" rolü eklemek, ROLE_PERMISSIONS'a bir satır eklemek demek; bu fonksiyonu değiştirmek değil.
  • Sahiplik kontrolü (document.authorId === session.userId) hala bir if/else olarak kalıyor. RBAC "sadece kendinize ait olanlar" ifadesini karşılayamaz. Bu, açıkça RBAC'ın sınırlaması ve Post 104'teki ABAC'ın motivasyonudur.

Ayrıntılı İzinler

CRUD Ötesi: Eylemleri Genişletme

Bir uygulama büyüdükçe basit CRUD tüm operasyonları kapsamaz. Diziye yeni eylemler eklenebilir:

typescript
export const ACTIONS = [  'create',  'read',  'update',  'delete',  'publish',  'archive',  'invite-member',  'manage-settings',] as const;

Permission template literal türü otomatik olarak genişler. document:archive, project:invite-member ve project:manage-settings ek tür tanımı olmadan geçerli izin dizgileri haline gelir.

Departman Bazlı İzin Yardımcısı

Kuruluşlarda departmanlar olduğunda, yaygın bir kalıp hem rol iznini HEM DE departman üyeliğini kontrol etmektir:

typescript
function canEditInDepartment(  session: { userId: string; role: Role; departmentId: string },  document: DocumentWithProject): boolean {  // Adım 1: Rolün bu izni var mı?  if (!can(session.role, 'document', 'update')) {    return false;  }
  // Adım 2: Kullanıcı aynı departmanda mı?  if (session.departmentId !== document.project.departmentId) {    // Admin departman kısıtlamasını atlar    if (session.role !== 'admin') return false;  }
  return true;}

Departman kontrolünün can() fonksiyonunun dışında olduğuna dikkat edin. RBAC'ın can() fonksiyonu yalnızca roller ve izinler hakkında bilgi sahibidir. "Hangi departman" veya "hangi belirli kaynak" kavramı yoktur. Bu, bir veya iki bağlamsal kontrol için işe yarar. Ancak bunlar çoğaldıkça (departman, sahiplik, zaman, belge durumu) yardımcı fonksiyonlar artar. Aynı soruna geri döneriz.

Sahiplik Bazlı İzin Yardımcısı

typescript
function canModifyDocument(  session: { userId: string; role: Role },  document: { authorId: string }): boolean {  // RBAC ile global izin kontrolü  if (can(session.role, 'document', 'update')) {    return true;  }
  // Sahiplik yedek kontrolü: yazarlar her zaman kendi belgelerini düzenleyebilir  return document.authorId === session.userId;}

Yine, sahiplik kontrolü RBAC'ın dışındadır. can() fonksiyonu "sadece kendi belgeleriniz" ifadesini karşılayamaz. Bu tasarım gereğidir. RBAC rolleri izinlere eşler, o kadar.

UI ve Backend Senkronizasyonu

Paylaşılan İzin Modülü

Temel kavram: lib/permissions.ts dosyasında 'server-only' import'u yoktur. Veritabanı erişimi, oturum yönetimi veya sunucuya özgü API içermez. Tür tanımları, sabit bir nesne ve saf fonksiyonlardan oluşan bir TypeScript modülüdür.

Bu, şunlar tarafından import edilebileceği anlamına gelir:

  • Service layer dosyaları (server-only): yetkilendirme için
  • React Server Component'ları: koşullu render için
  • React Client Component'ları: koşullu render için
  • Middleware: route düzeyinde kontroller için

Tek doğruluk kaynağı. Sıfır tekrar.

Server Component Kullanımı

Server Component'lar verifySession() çağırabilir ve can() fonksiyonunu doğrudan kullanabilir:

typescript
// components/document-actions.tsximport { verifySession } from '@/lib/auth';import { can, type Role } from '@/lib/permissions';
export async function DocumentActions({  document,}: {  document: DocumentDTO;}) {  const session = await verifySession();  if (!session) return null;
  const role = session.role as Role;
  return (    <div>      {can(role, 'document', 'update') && (        <EditButton documentId={document.id} />      )}      {can(role, 'document', 'delete') && (        <DeleteButton documentId={document.id} />      )}      {can(role, 'document', 'publish') && (        <PublishButton documentId={document.id} />      )}    </div>  );}

Post 102'nin versiyonuyla karşılaştırın:

typescript
// Post 102: tekrarlanan mantık, sapma eğilimliconst canEdit =  session.role === 'admin' ||  session.role === 'editor' ||  document.authorId === session.userId;
// Post 103: paylaşılan doğruluk kaynağıconst canEdit = can(role, 'document', 'update');

Bileşendeki can() çağrısı ve service layer'daki can() çağrısı aynı ROLE_PERMISSIONS nesnesini sorgular. Bir editör silme izni kazanırsa tek bir yerde değiştirin. Hem sunucu hem istemci değişikliği otomatik olarak yansıtır.

Geçirilen İzinlerle Client Component

Client Component'lar verifySession() çağıramaz. Çözüm: izinleri bir üst Server Component'ta çözümleyin ve prop olarak geçirin.

typescript
// Üst Server Componentimport { verifySession } from '@/lib/auth';import { can, type Role } from '@/lib/permissions';
export async function DocumentPage({  params,}: {  params: Promise<{ id: string }>;}) {  const { id } = await params;  const session = await verifySession();  const role = session?.role as Role;
  const permissions = {    canEdit: can(role, 'document', 'update'),    canDelete: can(role, 'document', 'delete'),    canPublish: can(role, 'document', 'publish'),  };
  return <DocumentToolbar permissions={permissions} />;}
typescript
// Alt Client Component'use client';
interface ToolbarProps {  permissions: {    canEdit: boolean;    canDelete: boolean;    canPublish: boolean;  };}
export function DocumentToolbar({ permissions }: ToolbarProps) {  return (    <div>      {permissions.canEdit && <EditButton />}      {permissions.canDelete && <DeleteButton />}      {permissions.canPublish && <PublishButton />}    </div>  );}

Client Component izin sistemi hakkında hiçbir şey bilmez. Boolean'lar alır ve buna göre render eder.

PermissionGate Bileşeni

Şablonlarda tekrarlanan izin kontrolleri için yeniden kullanılabilir bir sarmalayıcı standart kodu azaltır:

typescript
// components/permission-gate.tsximport { verifySession } from '@/lib/auth';import {  can,  type Role,  type Resource,  type Action,} from '@/lib/permissions';
export async function PermissionGate({  resource,  action,  children,  fallback = null,}: {  resource: Resource;  action: Action;  children: React.ReactNode;  fallback?: React.ReactNode;}) {  const session = await verifySession();  if (!session) return fallback;
  const role = session.role as Role;
  if (!can(role, resource, action)) {    return fallback;  }
  return children;}

Kullanım:

typescript
<PermissionGate resource="document" action="delete">  <DeleteButton documentId={document.id} /></PermissionGate>

Warning: UI izin kontrolleri yalnızca kullanıcı deneyimi içindir; güvenlik için değil. Gizlenmiş bir buton yine de doğrudan API isteğiyle çağrılabilir. Service layer güvenlik sınırı olmaya devam eder. UI kontrolleri kullanıcıların yapamayacakları eylemleri görmesini engeller; service layer bu eylemleri çalıştırmasını engeller.

RBAC Sınırlamaları: can() Yetmediğinde

Bağlamsal Kararlar

can(role, 'document', 'update') genel bir soruyu yanıtlar: "Editörler belgeleri güncelleyebilir mi?" Ancak asıl soru genellikle şudur: "Bu editör bu belirli belgeyi güncelleyebilir mi?" Bu şunlara bağlıdır:

  • Editör belgenin projesinin üyesi mi?
  • Belge düzenleme için kilitli mi?
  • Belge "inceleme" durumunda mı?
  • Kullanıcı belgenin yazarı mı?

Bunların hiçbiri rol-izin haritasında ifade edilemez. Kullanıcının, kaynağın ve ortamın niteliklerini gerektirirler.

typescript
// RBAC bunu yanıtlayabilir:can('editor', 'document', 'update'); // true
// RBAC bunu yanıtlayamaz:// "Editör Bob, XYZ belgesini güncelleyebilir mi?"// Bağlıdır: Bob'un proje üyeliği, XYZ'nin durumu, XYZ'nin yazarı

İzin Matrisi Patlaması

Gereksinimler büyüdükçe, ekipler giderek daha spesifik izinler oluşturur:

  • document:update: genel
  • document:update-own: sadece kendinize ait
  • document:update-in-department: sadece departmanınızdaki
  • document:update-draft: sadece taslaklar
  • document:update-published: sadece yayınlanmış belgeler

İzin matrisi patlar. Bu, NIST'in "rol patlaması" sorununun kod düzeyindeki karşılığıdır. Her uç durum için yeni roller oluşturmak yerine yeni izin dizgileri oluşturuyoruz; aynı sorunun farklı bir görünümü.

Yardımcı Fonksiyon Çoğalması

RBAC'ın yanında bağlamsal kontrolleri yönetmek için yardımcı fonksiyonlar çoğalır:

typescript
// Bunların hepsi can() ile birlikte varcanEditOwnDocument(session, document);canEditInDepartment(session, document);canPublishInReviewStatus(session, document);canDeleteIfNotLocked(session, document);canAccessDraftInProject(session, document, project);

Her biri özel bir if/else zinciridir. can() fonksiyonu rol kontrolünü yapar, ancak bağlamsal mantık hala zorunlu koddur. Post 102'den ilerleme kaydettik, ancak bağlamsal kontroller dağınık kalmaya devam ediyor.

RBAC Ne Zaman Kullanılır, Ne Zaman Ötesine Geçilir

RBAC, izinler birincil olarak kullanıcının rolüne bağlı olduğunda doğru araçtır: "Admin'ler her şeyi yapabilir", "Editörler güncelleyip yayınlayabilir", "Viewer'lar sadece okuyabilir." RBAC, izinler bağlama bağlı olduğunda yanlış araçtır: sahiplik, kaynak durumu, takım üyeliği veya çevresel koşullar.

Sırada Ne Var

can() fonksiyonu ve ROLE_PERMISSIONS haritası rol bazlı yetkilendirmeyi temiz bir şekilde çözer. Yeni bir rol eklemek tek satırlık bir değişikliktir. İzin matrisi incelenebilir ve denetlenebilir. Sunucu ve istemci tek bir doğruluk kaynağını paylaşır.

Ancak her bağlamsal kontrol (sahiplik, departman kapsamı, belge durumu) hala RBAC'ın dışında özel bir yardımcı fonksiyon olarak yaşar. Bunlar çoğaldıkça, sistem RBAC'ın sağladığı bildirimsel netliği kaybeder.

Post 104'te Nitelik Bazlı Erişim Kontrolü'nü (ABAC) tanıtıyoruz. can() fonksiyonu, kullanıcının, kaynağın ve ortamın niteliklerini bir politika motoru aracılığıyla değerlendirmeye evrilir. Sahiplik, departman kapsamı ve kaynak durumu politika kurallarına dönüşür; sabit kodlanmış koşullara değil.

typescript
// Önizleme -- tam uygulama Post 104'te// Şu an:if (can(session.role, 'document', 'update')) {  // sahiplik kontrolü hala ayrı}// Bunun yerine:if (evaluate(session, 'update', document)) {  // sahiplik, departman, durum -- hepsi tek bir politikada}

Service layer kalıyor. Mimari değişmiyor. Sadece izin kontrollerinin içindeki karar motoru gelişiyor.

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.

İlerleme3/6 yazı tamamlandı

İlgili Yazılar