2026-03-15
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 |
| 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 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.
// 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
Uygulama
Tek bir fonksiyon, tüm sabit kodlanmış rol kontrollerinin yerini alır.
// 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 doğal okunur.hasPermission(role, 'document:update')UI bileşenlerinde doğal okunur. İkisi de aynıROLE_PERMISSIONSharitasını sorgular.
Öncesi ve Sonrası: Service Layer Yeniden Düzenleme
Ö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,
ROLE_PERMISSIONS’a bir satır eklemek demek; bu fonksiyonu değiştirmek değil. - 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ı İzin Yardımcısı
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ı İzin Yardımcısı
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:
// 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.
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.
// Ü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 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.
// 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 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.
// Ö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
- NIST Role-Based Access Control (RBAC) Project - 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) - 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 - 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 - 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 -
ROLE_PERMISSIONSkalıbında kullanılansatisfiesoperatörü için resmi dokümantasyon - TypeScript Template Literal Types -
Permissiontürünü oluşturan template literal türleri için resmi dokümantasyon - RBAC vs ABAC vs ReBAC (Web Dev Simplified) - TypeScript’te kod örnekleriyle üç izin modelinin pratik karşılaştırması
- Role Explosion: The Hidden Cost of RBAC (Permify) - Rol patlaması nedenleri, bakım etkisi ve ince taneli erişim kontrolü ile çözümlerinin analizi
- CASL - Isomorphic Authorization JavaScript Library -
can()fonksiyon kalıbına ilham veren kütüphane; izomorfik, TypeScript-native yetkilendirme - Next.js Authentication Guide (Official) - Data Access Layer kalıbı ve server-only yetkilendirmeyi kapsayan resmi Next.js dokümantasyonu
- Role-Based Access Control (Wikipedia) - 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.
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.
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'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.
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrarlanan izin mantığını ortadan kaldıran otomatik veritabanı sorgusu filtreleme ile genişletin.
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı bir kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini seçmek için karar çerçevelerini kullanın.