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.
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.
O zincirlerin yerini RBAC (Rol Bazlı Erişim Kontrolü) alıyor. Modeli Ferraiolo ve Kuhn 1992’de biçimlendirdi, NIST ise INCITS 359-2004 olarak standartlaştırdı. Tek bir type-safe can() fonksiyonu sabit kodlanmış kontrollerin tamamını devralıyor. Hem sunucu hem istemci tarafında çalışıyor ve Post 102’nin açık bıraktığı izin mantığı tekrarını kapatı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ılık işin özüdür: birisi görev değiştirdiğinde rolünü değiştirirsiniz, elli ayrı izin arkasından gelir.
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.
Örnek Domain’de RBAC#
NIST kavramlarını Post 101-102’deki domain’e eşleyelim:
| NIST Kavramı | Bizim Domainimiz |
|---|---|
| Kullanıcı | verifySession() ile doğrulanmış oturum |
| Rol | admin, editor, author, viewer |
| Nesne (Kaynak) | document, project |
| Operasyon (Eylem) | create, read, update, delete, publish |
| İzin | Rol-(kaynak, eylem) eşlemesi |
Kullanıcılar rollere, roller de izinlere bağlanır. Hiçbir kullanıcı doğrudan bir izne bağlanmaz; RBAC yapısının tamamı bu dolaylılıktan ibarettir.
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.
// 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#
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.
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 constliteral türleri korur; her izinstringdeğil'document:create'olarak kalırsatisfiesş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.
| İzin | admin | editor | author | viewer |
|---|---|---|---|---|
| document:create | evet | evet | evet | — |
| document:read | evet | evet | evet | evet |
| document:update | evet | evet | evet | — |
| document:delete | evet | — | — | — |
| document:publish | evet | evet | — | — |
| project:create | evet | — | — | — |
| project:read | evet | evet | evet | evet |
| project:update | evet | — | — | — |
| project:delete | evet | — | — | — |
Bu tablo izin sisteminin kendisidir. “Bir editör ne yapabilir?” sorusu tek bakışta cevaplanır.
can() Fonksiyonu#
İmplementasyon#
can() bir rol, bir kaynak ve bir eylem alır, ardından boolean döner.
// 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 kontrol
export 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 akıcı durur,hasPermission(role, 'document:update')ise UI bileşenlerinde. İkisi de aynıROLE_PERMISSIONSharitasını sorgular.
Öncesi ve Sonrası: Service Layer Refactor’ü#
Öncesi (Post 102’den sabit kodlanmış kontroller):
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):
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 trueifadesiif (can(session.role, 'document', 'update')) return trueoldu- Admin kontrolü artık özel durum değil.
adminrolünün izin haritasındadocument:updateolduğu için çalışıyor. - Belgeleri düzenleyebilen bir “moderator” rolü eklemek için
ROLE_PERMISSIONS’a tek satır yazmanız yeterli; bu fonksiyona dokunmazsınız. - Sahiplik kontrolü (
document.authorId === session.userId) hala birif/elseolarak 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:
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ı Yardımcı Fonksiyon#
Kuruluşlarda departmanlar olduğunda, yaygın bir kalıp hem rol iznini HEM DE departman üyeliğini kontrol etmektir:
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ı Yardımcı Fonksiyon#
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ü#
İşin püf noktası şu: lib/permissions.ts dosyasında 'server-only' import’u yok. 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
Dördü de aynı dosyayı import ettiği için tek bir doğruluk kaynağı vardır ve tekrar oluşmaz.
Server Component Kullanımı#
Server Component’lar verifySession() çağırabilir ve can() fonksiyonunu doğrudan kullanabilir:
// components/document-actions.tsx
import { 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:
// Post 102: tekrarlanan mantık, sapma eğilimli
const 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.
İzinleri Prop Olarak Alan Client Component#
Client Component’lar verifySession() çağıramaz. Çözüm: izinleri bir üst Server Component’ta çözümleyin ve prop olarak geçirin.
// Üst Server Component
import { 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} />;
}
// 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:
// components/permission-gate.tsx
import { 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:
<PermissionGate resource="document" action="delete">
<DeleteButton documentId={document.id} />
</PermissionGate>
Warning
UI izin kontrolleri bir kullanıcı deneyimi özelliğidir ve tek başına hiçbir güvenlik sağlamaz. Gizlenmiş bir buton yine de doğrudan API isteğiyle çağrılabilir; bu yüzden güvenlik sınırı service layer 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.
// 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: geneldocument:update-own: sadece kendinize aitdocument:update-in-department: sadece departmanınızdakidocument:update-draft: sadece taslaklardocument: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:
// Bunların hepsi can() ile birlikte var
canEditOwnDocument(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 Yeter, Ne Zaman Yetmez#
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.
Sonraki Adım#
Her bağlamsal kontrol (sahiplik, departman kapsamı, belge durumu) hala RBAC’ın dışında, ayrı bir yardımcı fonksiyon olarak duruyor. 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, motorun değerlendirdiği politika kurallarına dönüşür.
// Ö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 ve genel mimari olduğu gibi kalıyor; sadece izin kontrollerinin içindeki karar motoru gelişiyor.
Kaynaklar#
- NIST Role-Based Access Control (RBAC) Project (yeni sekmede açılır) - RBAC için NIST’in temel proje sayfası, resmi tanım ve INCITS 359 standart referansları
- The NIST Model for Role-Based Access Control (Sandhu, Ferraiolo, Kuhn, 2000) (yeni sekmede açılır) - Dört RBAC model seviyesini tanımlayan temel makale: düz, hiyerarşik, kısıtlı ve simetrik RBAC
- OWASP A01:2025 - Broken Access Control (yeni sekmede açılır) - Bozuk erişim kontrolü OWASP Top 10’da birinci sırada kalmaya devam ediyor; yapılandırılmış izin sistemlerinin neden kritik olduğunu pekiştiriyor
- OWASP Authorization Cheat Sheet (yeni sekmede açılır) - Merkezi yetkilendirme mantığı, varsayılan olarak reddetme ve en az ayrıcalık ilkesi için en iyi uygulamalar
- TypeScript 4.9: The satisfies Operator (yeni sekmede açılır) -
ROLE_PERMISSIONSkalıbında kullanılansatisfiesoperatörü için resmi dokümantasyon - TypeScript Template Literal Types (yeni sekmede açılır) -
Permissiontürünü oluşturan template literal türleri için resmi dokümantasyon - RBAC vs ABAC vs ReBAC (Web Dev Simplified) (yeni sekmede açılır) - TypeScript’te kod örnekleriyle üç izin modelinin pratik karşılaştırması
- Role Explosion: The Hidden Cost of RBAC (Permify) (yeni sekmede açılır) - Rol patlaması nedenleri, bakım etkisi ve ince taneli erişim kontrolü ile çözümlerinin analizi
- CASL - Isomorphic Authorization JavaScript Library (yeni sekmede açılır) -
can()fonksiyon kalıbına ilham veren kütüphane; izomorfik, TypeScript-native yetkilendirme - Next.js Authentication Guide (Official) (yeni sekmede açılır) - Data Access Layer kalıbı ve server-only yetkilendirmeyi kapsayan resmi Next.js dokümantasyonu
- Role-Based Access Control (Wikipedia) (yeni sekmede açılır) - RBAC tarihçesi, üç NIST kuralı ve RBAC0-RBAC3 model hiyerarşisinin kapsamlı 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.
Bu serideki tüm yazılar
İlgili yazılar
Authentication ve authorization farkı, yaygın izin sistemi tuzakları, fail-closed prensibi ve her izin sisteminin karşılaması gereken hedefler.
typescript · nextjs · authorization +2
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 · nextjs · authorization +2
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.
typescript · nextjs · authorization +2
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrar eden izin mantığını ortadan kaldıran otomatik sorgu filtrelemeyle genişletin.
typescript · nextjs · authorization +2
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini karar çerçeveleriyle seçin.
typescript · nextjs · authorization +3