İçeriğe atla

2026-03-15

Service Layer ile Merkezi Yetkilendirme

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.

Özet

Post 101’de dağınık izin kontrollerinin kök sorunlarını teşhis ettik: tutarsız mantık, güvenlik açıkları ve bakım yükü. Bu yazı ilk çözümü sunuyor — service layer pattern. Tüm yetkilendirme mantığı, uygulama kodunuz ile veritabanı arasında tek bir katmana taşınıyor. Next.js middleware ile route koruması ve birden fazla seviyede guard pattern’leriyle birleştiğinde, izin kontrollerinin bir kez ve doğru şekilde yapıldığı bir derinlemesine savunma mimarisi oluşuyor.

Dağınık Kontroller Sorunu — Kök Neden

Post 101’de aynı “bu kullanıcı bu kaynağa erişebilir mi?” sorusunun bir sayfa bileşeninde, bir sunucu eyleminde ve bir API route’unda farklı şekilde yanıtlandığı üç kod örneği gösterdik. Her birinde farklı bir hata vardı. Belirtiler tutarsızlık, eksik kontroller ve aşırı izindi.

Kök neden mimariseldir: tüm veri erişiminin geçmesi gereken bir darboğaz noktası yoktur. Sayfalar, sunucu eylemleri ve API route’ları veritabanına doğrudan erişir. İzin kontrolleri her giriş noktasına isteğe bağlı eklemelerdir. Geliştiricilerin bunları eklemeyi hatırlaması gerekir — ve “hatırlama” bir güvenlik stratejisi değildir.

Çözüm bu darboğaz noktasını oluşturmaktır. Her kapıya güvenlik görevlisi yerleştirip hiçbirinin eksik olmadığını ummak yerine, tüm trafiği tek bir kontrol noktasından geçirin.

Cozum: Service Layer

yetkili

reddedildi

Sayfa Bileseni

Service Layer

Sunucu Eylemi

API Route

Veritabani

Hata / Yonlendirme

Sorun: Dogrudan DB Erisimi

ad-hoc kontrol

farkli kontrol

kontrol yok

Sayfa Bileseni

Veritabani

Sunucu Eylemi

API Route

Service Layer Pattern

Tanım

Martin Fowler, Service Layer’ı “kullanılabilir operasyonlar kümesi oluşturan ve her operasyonda uygulamanın yanıtını koordine eden bir sınır” olarak tanımlar. Yetkilendirme bağlamında service layer, izin kararlarının verildiği ve uygulandığı tek yerdir.

Bu, Next.js’in resmi dokümantasyonundaki önerilen Data Access Layer (DAL) pattern’ine doğrudan karşılık gelir. Temel fikir aynıdır: üç sorumluluğu birleştiren, yalnızca sunucuda çalışan bir katman oluşturun:

  1. Kimlik doğrulama kontrolü — kullanıcı iddia ettiği kişi mi?
  2. Yetkilendirme kontrolleri — bu kullanıcı bu kaynakta bu eylemi gerçekleştirmeye yetkili mi?
  3. Veri erişimi ve dönüştürme — veriyi getir ve yalnızca kullanıcının görmesi gerekeni döndür.

Tek Sorumluluk Prensibi

Yaygın bir yanılgı, SRP’nin “bir fonksiyon bir şey yapar” anlamına geldiğidir. Aslında “bir modülün değişmek için tek bir nedeni olmalı” demektir. Service layer’ın değişme nedeni, izin kuralları veya veri erişim kalıpları değiştiğindedir. Sayfalar UI nedenleriyle değişir. Sunucu eylemleri form yönetimi nedenleriyle değişir. İkisi de yetkilendirme mantığı taşımamalıdır.

Mimari Genel Bakış

Veri Katmani

Service Layer (server-only)

Uygulama Giris Noktalari

Server Component

Server Action

API Route Handler

verifySession()

DocumentService

ProjectService

Veritabani / ORM

Teknik detaylar:

  • server-only paketi, service layer kodunun istemci bileşenlerine dahil edilmesini engeller. Client Component tarafından import edilen bir dosyada bu paketi import etmek derleme hatasına neden olur.
  • React’ın cache() fonksiyonu, oturum doğrulamasını tek bir istek/render geçişi içinde memoize eder. Birden fazla bileşenin verifySession() çağırması yalnızca tek bir çerez çözme işlemiyle sonuçlanır.

Refactoring: Dağınık Kontrollerden Merkezi Servise

Adım 1: Oturum Doğrulama

Temel yapı, memoize edilmiş bir oturum doğrulama aracıdır. Her servis metodu önce bunu çağırır.

// lib/auth.ts
import 'server-only';
import { cache } from 'react';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

// Tek bir istek/render geçişi içinde memoize edilir
export const verifySession = cache(async () => {
  const cookieStore = await cookies();
  const token = cookieStore.get('session')?.value;

  if (!token) {
    return null;
  }

  try {
    const session = await decryptAndValidate(token);
    return { userId: session.userId, role: session.role };
  } catch {
    return null; // Fail closed -- geçersiz token = oturum yok
  }
});

// Kimliği doğrulanmamış kullanıcıları yönlendiren yardımcı
export const requireSession = cache(async () => {
  const session = await verifySession();
  if (!session) {
    redirect('/login');
  }
  return session;
});

İki fonksiyon, iki amaç. verifySession() kimliği doğrulanmamış istekler için null döndürür — giriş yapmış ve anonim kullanıcılar için farklı render yapan bileşenler için kullanışlıdır. requireSession() hemen yönlendirir — korunan sayfalar ve servis metotları için kullanışlıdır.

Adım 2: Özel Hata Sınıfları

Servisleri oluşturmadan önce yapılandırılmış hata tipleri tanımlayın. Bunlar genel throw new Error('Unauthorized') çağrılarını belirli, yakalanabilir hatalarla değiştirir.

// lib/errors.ts
export class UnauthorizedError extends Error {
  constructor(message = 'Authentication required') {
    super(message);
    this.name = 'UnauthorizedError';
  }
}

export class ForbiddenError extends Error {
  constructor(message = 'Access denied') {
    super(message);
    this.name = 'ForbiddenError';
  }
}

export class NotFoundError extends Error {
  // Kaynak numaralandırmasını önlemek için genel mesaj
  constructor(message = 'Resource not found or access denied') {
    super(message);
    this.name = 'NotFoundError';
  }
}

Tip: NotFoundError kasıtlı olarak genel bir mesaj kullanır. “Document not found” ile “Access denied” döndürmek, kaynağın var olup olmadığını ortaya çıkarır. Hassas kaynaklar için ikisini tek bir mesajda birleştirerek numaralandırma saldırılarını önleyin.

Adım 3: DocumentService

Bu, temel refactoring adımıdır. Post 101’deki dağınık kontrolleri merkezi servis ile karşılaştırın.

Önce — yetkilendirme sunucu eyleminde dağınık (Post 101’den):

// ÖNCE: İzin kontrolü sunucu eyleminde dağınık
'use server';
export async function updateDocument(
  documentId: string,
  content: string
) {
  const session = await getSession();
  const document = await db.document.findUnique({
    where: { id: documentId },
  });
  if (document.authorId !== session.userId) {
    // Hata: editörlerin de düzenleyebildiğini unutmuş
    throw new Error('Unauthorized');
  }
  // Hata: proje üyeliği kontrolü yok
  await db.document.update({
    where: { id: documentId },
    data: { content },
  });
}

Sonra — service layer’da merkezi:

// lib/services/document-service.ts
import 'server-only';
import { requireSession } from '@/lib/auth';
import { ForbiddenError, NotFoundError } from '@/lib/errors';

export async function getDocumentById(documentId: string) {
  const session = await requireSession();

  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: { include: { members: true } } },
  });

  if (!document) {
    throw new NotFoundError();
  }

  if (!canAccessDocument(session, document)) {
    throw new ForbiddenError();
  }

  return toDocumentDTO(document, session);
}

export async function updateDocument(
  documentId: string,
  content: string
) {
  const session = await requireSession();

  const document = await db.document.findUnique({
    where: { id: documentId },
    include: { project: { include: { members: true } } },
  });

  if (!document) {
    throw new NotFoundError();
  }

  if (!canEditDocument(session, document)) {
    throw new ForbiddenError();
  }

  const updated = await db.document.update({
    where: { id: documentId },
    data: { content },
  });

  return toDocumentDTO(updated, session);
}

export async function listDocumentsByProject(
  projectId: string
) {
  const session = await requireSession();

  const project = await db.project.findUnique({
    where: { id: projectId },
    include: { members: true },
  });

  if (!project) {
    throw new NotFoundError();
  }

  if (!canAccessProject(session, project)) {
    throw new ForbiddenError();
  }

  const documents = await db.document.findMany({
    where: { projectId },
  });

  // Belgeleri kullanıcının erişim seviyesine göre filtrele
  return documents
    .filter((doc) => canAccessDocument(session, {
      ...doc,
      project,
    }))
    .map((doc) => toDocumentDTO(
      { ...doc, project },
      session
    ));
}

İzin mantığı, odaklanmış yardımcı fonksiyonlara çıkarıldı:

// document-service.ts içindeki izin yardımcıları

function canAccessDocument(
  session: { userId: string; role: string },
  document: DocumentWithProject
): boolean {
  if (session.role === 'admin') return true;

  const project = document.project;

  // Public projeler: yayınlanmış belgeler herkese açık
  if (
    project.visibility === 'public' &&
    document.status === 'published'
  ) {
    return true;
  }

  // Diğer her şey için proje üyesi olmalı
  const membership = project.members.find(
    (m) => m.userId === session.userId
  );
  if (!membership) return false;

  // Üyeler yayınlanmış belgeleri görebilir
  if (document.status === 'published') return true;

  // Yazarlar kendi taslaklarını görebilir
  if (document.authorId === session.userId) return true;

  // Editörler taslaklar dahil tüm belgeleri görebilir
  if (membership.role === 'editor') return true;

  return false; // Fail closed
}

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;

  // Editörler projedeki herhangi bir belgeyi düzenleyebilir
  if (membership.role === 'editor') return true;

  // Yazarlar kendi belgelerini düzenleyebilir
  if (
    membership.role === 'author' &&
    document.authorId === session.userId
  ) {
    return true;
  }

  return false; // Fail closed
}

Pattern’e dikkat edin. Her servis metodu aynı yapıyı izler:

  1. Oturumu doğrula
  2. Kaynağı ilişkili verilerle getir
  3. Yetkilendirmeyi kontrol et
  4. Filtrelenmiş DTO döndür

Adım 4: ProjectService

Aynı pattern projeler için de geçerlidir. Kaynak başına bir servis dosyası, kod tabanını düzenli tutar.

// lib/services/project-service.ts
import 'server-only';
import { requireSession } from '@/lib/auth';
import { ForbiddenError, NotFoundError } from '@/lib/errors';

export async function getProjectById(projectId: string) {
  const session = await requireSession();

  const project = await db.project.findUnique({
    where: { id: projectId },
    include: { members: true },
  });

  if (!project) {
    throw new NotFoundError();
  }

  if (!canAccessProject(session, project)) {
    throw new ForbiddenError();
  }

  return toProjectDTO(project, session);
}

export async function listProjectsForUser() {
  const session = await requireSession();

  if (session.role === 'admin') {
    const projects = await db.project.findMany({
      include: { members: true },
    });
    return projects.map((p) => toProjectDTO(p, session));
  }

  const projects = await db.project.findMany({
    where: {
      OR: [
        { visibility: 'public' },
        { members: { some: { userId: session.userId } } },
        { ownerId: session.userId },
      ],
    },
    include: { members: true },
  });

  return projects.map((p) => toProjectDTO(p, session));
}

function canAccessProject(
  session: { userId: string; role: string },
  project: ProjectWithMembers
): boolean {
  if (session.role === 'admin') return true;
  if (project.visibility === 'public') return true;
  if (project.ownerId === session.userId) return true;

  const membership = project.members.find(
    (m) => m.userId === session.userId
  );
  return !!membership; // Fail closed
}

Adım 5: Güvenli Veri Transferi için DTO Pattern

Ham veritabanı nesneleri istemciye ulaşmaması gereken alanlar içerir. Dahili ID’ler, zaman damgaları, soft-delete flag’leri ve diğer rollere yönelik meta veriler, veritabanı modelini doğrudan döndürdüğünüzde sızar.

DTO (Data Transfer Object) pattern’i bunu çözer. Her servis metodu, kullanıcının rolüne göre filtrelenmiş bir nesne döndürür.

// lib/services/document-service.ts

interface DocumentDTO {
  id: string;
  title: string;
  content: string;
  status: string;
  projectId: string;
  authorId: string;
  reviewComments?: string[];
}

function toDocumentDTO(
  document: DocumentWithProject,
  session: { userId: string; role: string }
): DocumentDTO {
  const membership = document.project.members.find(
    (m) => m.userId === session.userId
  );

  const base: DocumentDTO = {
    id: document.id,
    title: document.title,
    content: document.content,
    status: document.status,
    projectId: document.projectId,
    authorId: document.authorId,
  };

  // Editörler ve adminler inceleme meta verilerini görür
  if (
    session.role === 'admin' ||
    membership?.role === 'editor'
  ) {
    return {
      ...base,
      reviewComments: document.reviewComments,
    };
  }

  return base;
}

Bu, Post 101’deki aşırı izin sorununu çözer. Viewer başlık ve içerik alır. Editör başlık, içerik ve inceleme yorumlarını alır. Aynı kaynak, farklı görünümler — tek bir yerde kontrol ediliyor.

Adım 6: İnce Sunucu Eylemleri

Service layer yerindeyken, sunucu eylemleri ince sarmalayıcılar haline gelir. Form verilerini yönetir ve servisi çağırır. Başka bir şey yok.

// app/actions/documents.ts
'use server';
import { updateDocument } from '@/lib/services/document-service';

export async function updateDocumentAction(
  documentId: string,
  content: string
) {
  // Tüm yetkilendirme servis içinde gerçekleşir
  return updateDocument(documentId, content);
}

Bunu Post 101’deki hatalı sunucu eylemiyle karşılaştırın. Eylem artık hiçbir izin mantığı içermiyor. İzin hataları olamaz çünkü her şeyi service layer’a devreder.

Route Koruması için Next.js Middleware

Not: Next.js 16’da middleware.ts dosyası proxy.ts olarak, export edilen fonksiyon da proxy olarak yeniden adlandırıldı. Bu bölümdeki pattern’ler ve konseptler her ikisi için de geçerlidir. Next.js 15 veya daha eski sürümlerde middleware.ts kullanın.

Middleware Ne Yapmalı

Middleware, eşleşen her istekten önce çalışır. Hızlıdır ve çerezlere ve header’lara erişimi vardır. Bu onu tek bir iş için uygun kılar: iyimser kimlik doğrulama kontrolleri.

  • Oturum çerezinin var olup olmadığını kontrol etme
  • Kimliği doğrulanmamış kullanıcıları korunan route’lardan uzaklaştırma
  • Oturum token’ından kaba taneli rol iddialarını kontrol etme (“bu kullanıcı admin mi?” gibi)
  • İlk savunma katmanı olarak hareket etme — son değil

Middleware Ne YAPMAMALI

  • Tek yetkilendirme mekanizması olmamalı (CVE-2025-29927 nedenini gösterdi)
  • İzin kontrolleri için veritabanı sorgusu yapmamalı (performans sorunu — her istekte çalışır)
  • Kaynak düzeyinde yetkilendirme yapmamalı (middleware kaynak bağlamına sahip değil — kullanıcının hangi belgeye erişmeye çalıştığını bilmez)

Uygulama

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { decrypt } from '@/lib/session';

const publicRoutes = ['/', '/login', '/signup'];
const adminRoutes = ['/admin'];

export async function middleware(request: NextRequest) {
  const path = request.nextUrl.pathname;

  // Public route'lara kimlik doğrulama olmadan izin ver
  if (publicRoutes.some((route) => path === route)) {
    return NextResponse.next();
  }

  // Oturum çerezini kontrol et (iyimser -- DB çağrısı yok)
  const cookie = request.cookies.get('session')?.value;
  const session = await decrypt(cookie);

  if (!session?.userId) {
    return NextResponse.redirect(
      new URL('/login', request.url)
    );
  }

  // Admin route'lar: oturum token'ından rolü kontrol et
  if (adminRoutes.some((route) => path.startsWith(route))) {
    if (session.role !== 'admin') {
      return NextResponse.redirect(
        new URL('/', request.url)
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|.*\\.png$).*)',
  ],
};

Bu middleware kasıtlı olarak basittir. Çerez varlığını ve oturum geçerliliğini kontrol eder. Veritabanı sorgusu yapmaz. Kaynak düzeyinde yetkilendirme yapmaz. Bu sorumluluklar service layer’a aittir.

Derinlemesine Savunma

Tek bir katman yeterli değildir. Her katman, diğerlerindeki potansiyel arızaları telafi eder.

Oturum cerezi yok

Oturumu var

Yetkili degil

Yetkili

Gelen Istek

Katman 1: Middleware

Login'e yonlendir

Sayfa / Eylem / API Route

Katman 2: Service Layer

403 Forbidden

Katman 3: Veritabani

Yanit (filtrelenmis DTO)

  • Katman 1 — Middleware: Kimliği doğrulanmamış isteklerin hızlı reddi. Oturum çerezlerine dayalı iyimser kontrol.
  • Katman 2 — Service Layer: Tam kaynak bağlamıyla kesin yetkilendirme. Kullanıcı kimliği, rol, proje üyeliği ve belge sahipliğini kontrol eder.
  • Katman 3 — Veritabanı: Foreign key’ler ve satır düzeyi güvenlik politikaları (gelecek bir yazıda ele alınacak) gibi kısıtlamalar son bir güvenlik ağı sağlar.

Middleware atlanırsa (CVE-2025-29927’deki gibi), service layer yetkisiz erişimi engellemeye devam eder. Service layer’da bir hata yanlış kaynağa erişim verirse, veritabanı kısıtlamaları tutarsızlığı yakalayabilir. Her katman bağımsız olarak koruma sağlar.

Guard Pattern’leri

Route Düzeyi Guard’lar (Middleware)

Yukarıda ele alındı. Middleware, sayfa render edilmeden önce kimliği doğrulanmamış kullanıcıları yönlendirir. Bu kaba taneldir: “Kullanıcı giriş yapmış mı?” ve “Bu korunan bir route mu?”

Sayfa Düzeyi Guard’lar (Server Component’ler)

Sayfa bileşenleri servis metotlarını doğrudan çağırır. Servis, kimlik doğrulama, yetkilendirme ve veri erişimini tek bir çağrıda yönetir.

// app/projects/[id]/page.tsx
import { getProjectById } from '@/lib/services/project-service';

export default async function ProjectPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  // Servis yönetir: oturum + yetkilendirme + veri erişimi
  const project = await getProjectById(id);

  return <ProjectView project={project} />;
}

Bunu Post 101’deki sürümle karşılaştırın — sayfa bileşeni tüm izin mantığını satır içi olarak içeriyordu. Sayfa artık ince. Servisi çağırır ve sonucu render eder. Kullanıcı bu projeye erişemiyorsa, servis hata fırlatır — sayfa değil.

Bileşen Düzeyi Guard’lar (Koşullu Render)

Server Component’ler verifySession() çağırabilir ve role göre koşullu render yapabilir. Bu yalnızca UI/UX amaçlıdır — güvenlik değil. Service layer güvenlik sınırıdır.

// components/document-actions.tsx
import { verifySession } from '@/lib/auth';

export async function DocumentActions({
  document,
}: {
  document: DocumentDTO;
}) {
  const session = await verifySession();
  if (!session) return null;

  const canEdit =
    session.role === 'admin' ||
    session.role === 'editor' ||
    document.authorId === session.userId;

  return (
    <div>
      {canEdit && <EditButton documentId={document.id} />}
      {session.role === 'admin' && (
        <DeleteButton documentId={document.id} />
      )}
    </div>
  );
}

Warning: Bu bileşen düzeyi kontrolü service layer mantığını kopyalar. Servisteki düzenleme izin kuralı değişirse, burada da değişmelidir. Bu çoğaltma, mevcut yaklaşımın bilinen bir sınırlamasıdır. Post 103 (RBAC), izin kurallarını tek bir kaynaktan sorgulanabilir hale getirerek bu sorunu çözer.

Sunucu Eylemi Guard’ları (Higher-Order Wrapper)

withAuth higher-order fonksiyonu, sunucu eylemlerini kimlik doğrulamayla sarar. Bu, her eylemin yürütülmeden önce oturumu doğrulamasını sağlar.

// lib/action-guard.ts
import { requireSession } from '@/lib/auth';

type ActionFn<TInput, TOutput> = (
  session: { userId: string; role: string },
  input: TInput
) => Promise<TOutput>;

export function withAuth<TInput, TOutput>(
  action: ActionFn<TInput, TOutput>
) {
  return async (input: TInput): Promise<TOutput> => {
    const session = await requireSession();
    return action(session, input);
  };
}

Sunucu eyleminde kullanım:

// app/actions/documents.ts
'use server';
import { withAuth } from '@/lib/action-guard';
import { updateDocument } from '@/lib/services/document-service';

export const updateDocumentAction = withAuth(
  async (session, input: { documentId: string; content: string }) => {
    return updateDocument(input.documentId, input.content);
  }
);

withAuth sarmalayıcısı, eylem gövdesi çalışmadan önce oturumun var olduğunu garanti eder. Servis metodu içindeki yetkilendirmeyle birleştiğinde, her sunucu eylemi için iki katmanlı koruma sağlar.

Test Edilebilirlik

Service layer’ın en büyük kazanımlarından biri test edilebilirliktir. İzin mantığı render, routing ve istek yönetiminden izole edilmiştir. Testlerin girdileri ve çıktıları nettir.

// __tests__/services/document-service.test.ts
import { getDocumentById } from '@/lib/services/document-service';
import { ForbiddenError } from '@/lib/errors';

// Farklı kullanıcı rolleri için oturumu mockla
jest.mock('@/lib/auth', () => ({
  requireSession: jest.fn(),
}));

describe('getDocumentById', () => {
  it('admin herhangi bir belgeye erişebilir', async () => {
    mockSession({ userId: 'admin-1', role: 'admin' });
    mockDocument({ id: 'doc-1', status: 'draft' });

    const result = await getDocumentById('doc-1');
    expect(result.id).toBe('doc-1');
  });

  it('viewer taslak belgelere erişemez', async () => {
    mockSession({ userId: 'viewer-1', role: 'viewer' });
    mockDocument({
      id: 'doc-1',
      status: 'draft',
      authorId: 'author-1',
    });

    await expect(
      getDocumentById('doc-1')
    ).rejects.toThrow(ForbiddenError);
  });

  it('yazar kendi taslağına erişebilir', async () => {
    mockSession({ userId: 'author-1', role: 'author' });
    mockDocument({
      id: 'doc-1',
      status: 'draft',
      authorId: 'author-1',
    });

    const result = await getDocumentById('doc-1');
    expect(result.id).toBe('doc-1');
  });
});

Her test, belirli bir izin kuralını izole olarak doğrular. Sayfa render’ı yok. İstek bağlamı yok. Form yönetimi yok. Sadece: “Kullanıcı X, rol Y ile belge Z’ye eriştiğinde, izin ver veya reddet.”

Dağınık kontrolleri test etmeyle karşılaştırın. Sayfaları render etmeniz, tam istek bağlamlarıyla sunucu eylemlerini çağırmanız ve kimlik doğrulama middleware’ini kurmanız gerekirdi. Service layer, en kritik mantığı — izinleri — test etmesi en kolay kılar.

Faydalar ve Sınırlamalar

Service Layer’ın Çözdükleri

Post 101 SorunuService Layer Çözümü
3+ konumda dağınık kontrollerKaynak işlemi başına tek konum
Katmanlar arası tutarsız mantıkTek uygulama, her yerden çağrılır
Yeni endpoint’lerde eksik kontrollerYeni endpoint’ler servis metotlarını çağırmalı
Aşırı izin (çok fazla veri döndürme)DTO pattern’i verileri role göre filtreler
İzin mantığını test etme zorluğuServis metotları bağımsız test edilebilir
Tek doğruluk kaynağı yokService layer doğruluk kaynağı OLUR

Hala Çözülmemiş Olanlar

Service layer, izin kontrollerinin nerede yapıldığını çözer. Ancak izin kurallarının kendisi hala sabit kodlanmış if/else ifadeleridir. Bu birkaç sınırlama oluşturur:

  1. Sabit kodlanmış rol kontrolleri: İzin mantığı, servis metotları içinde if (role === 'admin') şeklindedir. Yeni bir rol eklemek her ilgili servis metodunu değiştirmeyi gerektirir.
  2. İzin matrisi yok: “Rol X, kaynak Z üzerinde eylem Y’yi yapabilir” şeklinde bildirimsel, incelenebilir bir liste yoktur. İzin kuralları kod mantığına gömülüdür.
  3. Bileşen düzeyi çoğaltma: UI guard kontrolleri service layer mantığını kopyalar. Bunları senkronize tutmak manuel ve hataya açıktır.
  4. Rol değişiklikleri kod değişikliği gerektirir: Yayınlayabilen ama silemeyen bir “moderator” rolü eklemek birden fazla servis metoduna dokunmayı gerektirir.
  5. Denetim izi yok: Service layer izinleri uygular ama kimin neye ne zaman eriştiğini kaydetmez.

Bu sınırlamalar doğrudan bir sonraki adıma işaret eder: sabit kodlanmış if/else ifadelerini yapılandırılmış bir izin sistemiyle değiştirmek.

Yaygın Tuzaklar

  1. “Sadece bu seferlik” veritabanı atlama: Service layer dışındaki her doğrudan veritabanı erişimi potansiyel bir yetkilendirme açığıdır. Pattern’i kod incelemesiyle uygulayın ve servis dosyaları dışındaki doğrudan veritabanı import’larını işaretleyen özel bir ESLint kuralı düşünün.

  2. server-only eklemeyi unutmak: Bu import olmadan, service layer kodu bir Client Component tarafından import edilebilir. Bu, dahili mantığı ve veritabanı erişim kalıplarını istemci paketine sızdırır.

  3. Oturum doğrulama için cache() kullanmamak: Memoizasyon olmadan, bir sayfa bileşeninde ve bir alt sunucu bileşeninde verifySession() çağırmak istek başına iki çerez çözme işlemiyle sonuçlanır.

  4. Ham veritabanı nesneleri döndürmek: Servis metotlarından her zaman DTO döndürün. Ham nesneler zaman damgaları, soft-delete flag’leri veya istemciye ulaşmaması gereken dahili alanlar içerebilir.

  5. Kaynak varlığını sızdıran genel hata mesajları: throw new Error('Document not found') kaynağın var olmadığını ortaya çıkarır. Hassas kaynaklar için numaralandırmayı önlemek üzere “not found or access denied” kullanın.

Sıradaki

Service layer, yetkilendirme için doğru mimaridir. Tüm veri erişimi izin-farkında servisler üzerinden akar. Middleware ek bir savunma katmanı sağlar. Guard pattern’leri birden fazla seviyede oluşturulur.

Ancak service layer içindeki izin kuralları hala sabit kodlanmış if/else ifadeleridir. Bir kuralı değiştirmek kod değiştirmek demektir. Bir rol eklemek her servis metoduna dokunmak demektir. Tüm izinleri bir bakışta incelemenin yolu yoktur.

Post 103’te Rol Bazlı Erişim Kontrolü’nü (RBAC) tanıtacağız. Sabit kodlanmış koşullar yerine, izin kuralları veri haline gelir — hem service layer’ın hem de UI’ın sorgulayabileceği yapılandırılmış bir matris.

// Ön izleme -- tam uygulama Post 103'te
// Bunun yerine:
if (role === 'admin' || role === 'editor') {
  // düzenlemeye izin ver
}
// Şunu kullanacağız:
if (hasPermission(session.role, 'document', 'update')) {
  // düzenlemeye izin ver
}

Service layer kalır. Yalnızca izin kontrollerinin iç yapısı değişir.

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.

İlerleme 2 / 6 yazı

İlgili yazılar