Muzibu Admin Panel 403 Hatası

v2

Spatie Permission + Tenant Redis Prefix Mismatch

Tarih
9 Ocak 2026

📌 v2 Güncellemeleri

🚨

Sorun Özeti

Muzibu (Tenant 1001) admin paneline ve Horizon'a giriş yapılamıyordu. Database'de ROOT yetkisi olmasına rağmen sürekli 403 Forbidden hatası alınıyordu. Sorun iki kez yaşandı: İlk düzeltmeden sonra tekrar etti.

403 Forbidden Admin Panel Horizon Spatie Permission Tenant Redis

📝 Basit Anlatım (Herkes İçin)

Ne Oldu?

Site yöneticileri (nurullah@nurullah.net ve info@turkbilisim.com.tr) admin paneline girmeye çalıştıklarında "Erişim Reddedildi" hatası alıyorlardı. Oysa database'de (veritabanı) tam yetkileri vardı. Sorun düzeltildi ama birkaç saat sonra tekrar etti.

Neden Oldu?

Laravel'in yetki sistemi (Spatie Permission), kullanıcı yetkilerini hızlı erişim için geçici hafızada (cache) tutuyor. Bu geçici hafıza bozulmuştu ve kullanıcıların yetkilerini göremiyordu.

Daha Derin Sebep: Sistemde her müşteri (tenant) için ayrı geçici hafıza bölümü var. Muzibu için "tenant1001_" başlığı var. Ama yetki sistemi yetkiyi kaydederken bu başlığı koymadı, okurken aradı. Sanki "Anahtar Muzibu Kasası'nda" diye etiket koyulmadan kasaya konulan para, sonra "Muzibu Kasası" diye aranınca bulunamadı.

Neden Tekrar Etti?

Sistem ayarları yeniden yüklendiğinde (config cache) veya yetkilerde değişiklik yapıldığında, aynı etiket sorunu tekrar oluşuyor. Geçici çözüm işe yarıyor ama kalıcı değil.

Nasıl Çözüldü?

  1. Bozuk geçici hafıza tamamen temizlendi (Redis FLUSHDB)
  2. Veritabanından yetkileri tekrar okutup doğru etiketle kaydedildi (syncRoles)
  3. Kullanıcıların eski oturumu kapatıldı (session silindi)
  4. Yeni oturum açtıklarında artık yeni, doğru yetkiler yüklendi

✅ Geçici Çözüm: Logout yapıp yeniden login olduktan sonra her şey düzeldi. Admin panel ve Horizon'a giriş yapılabildi.

⚠️ Uyarı: Bu geçici bir çözüm. Sistem ayarları yenilendiğinde veya yetki değişikliğinde sorun tekrar edebilir. Kalıcı çözüm için sistem yapılandırması değiştirilmeli (detaylar aşağıda).

🔍 Kök Sebep Analizi (v2'de Bulundu)

1. Tenant Redis Prefix Sistemi

config/tenancy.php:
'redis' => [ 'prefix_base' => 'tenant', 'prefixed_connections' => ['default', 'cache'], ],

Bu ayar, her tenant için Redis key'lerinin başına tenant{id}_ prefix'i ekler. Örnek: Muzibu (Tenant 1001) için tenant1001_

2. Spatie Permission Cache Yapılandırması

config/permission.php:
'cache' => [ 'key' => 'spatie.permission.cache', 'store' => 'default', // Laravel default cache (Redis) ],

Spatie Permission, cache key olarak sabit spatie.permission.cache kullanıyor. Store olarak default (Redis) seçilmiş.

3. Sorunun Oluşum Süreci

1
Config Cache Yapılıyor
php artisan config:cache çalıştırılıyor. Bu sırada tenant context henüz initialize olmadı.
2
Spatie Cache Oluşturuluyor
Spatie Permission cache key'i oluşturuluyor:
Redis key: spatie.permission.cache
Tenant prefix YOK! Çünkü tenant context henüz aktif değildi.
3
Tenant Initialize Oluyor
Kullanıcı muzibu.com adresine giriyor. Tenant middleware devreye giriyor, Tenant 1001 initialize oluyor. Redis prefix artık tenant1001_
4
Permission Kontrol Ediliyor
Middleware $user->hasRole('root') çalıştırıyor. Spatie Permission cache'ten okumaya çalışıyor:
Aranan key: tenant1001_spatie.permission.cache
5
BULAMIYOR! ❌
Redis'te spatie.permission.cache var, ama tenant1001_spatie.permission.cache YOK!
Sonuç: Cache miss → Database'den okumaya çalışıyor → Yine cache'e yazıyor ama yine prefix mismatch → Döngü devam ediyor → $user->roles = []403 Forbidden

4. Redis Key Mismatch (Görsel)

❌ Cache Yazılırken (Prefix YOK)
Config cache sırasında:
spatie.permission.cache
Tenant context yok → Prefix eklenmedi
🔍 Cache Okunurken (Prefix VAR)
Tenant context'te:
tenant1001_spatie.permission.cache
Tenant context var → Prefix eklendi → BULAMIYOR!
💡 Özetle: Bir key iki farklı isimle aranıyor. Biri spatie.permission.cache, diğeri tenant1001_spatie.permission.cache. Redis'te sadece prefix'siz olan var, prefix'li olan yok. O yüzden bulamıyor.

🔧 Teknik Detaylar (Geliştiriciler İçin)

Database Doğru'ydu

-- model_has_roles tablosu
role_id: 1 (root)
model_id: 1, 2
model_type: 'App\\Models\\User'
-- role_has_permissions tablosu
ROOT role → 108 permission (hepsi)

Dosya: tenant_muzibu_1528d0 database (tenant context)

Spatie Permission Cache Bozuktu

Tinker Test Sonucu:
$user = User::find(1);
$user->roles->count(); // 0 (YANLIŞ!)
$user->hasRole('root'); // false (YANLIŞ!)
$user->isRoot(); // false (YANLIŞ!)

Database'de rol atanmış olmasına rağmen Laravel runtime'da role relationship boş dönüyordu. Sebep: Spatie Permission cache'i Redis'te tenant prefix mismatch ile bozuk/okunamaz durumdaydı.

Session'da Eski User Object

Login olunduğunda user object session'a kaydedilmişti. Bu object'te roles = [] (boş array) vardı. Her sayfa yüklendiğinde bu eski session'dan okunuyordu, dolayısıyla cache temizleme bile yeterli olmadı.

Session flow:
1. Login → User cached (roles = [])
2. Cache clear → Session'da user hala eski
3. Her request → Session'dan eski user yüklenir
4. Sonuç: 403 Forbidden

Uygulanan Geçici Çözüm

1

Tinker ile Role Sync

// Tenant context'te
$user1 = User::find(1);
$user1->syncRoles(['root']);
$user2 = User::find(2);
$user2->syncRoles(['root']);

Etki: Spatie Permission cache'ini temizler ve role relationship'i fresh yükler

2

Redis FLUSHDB

redis-cli FLUSHDB

Etki: Tüm Spatie cache + Laravel session + diğer cache'ler tamamen silindi (prefix'li ve prefix'siz hepsi)

3

Cache Rebuild

php artisan cache:clear
php artisan config:clear
php artisan view:clear
curl https://ixtif.com/opcache-reset.php

Etki: Config cache rebuild + OPcache (PHP bytecode) reset

4

Logout + Login (Kullanıcı)

Kullanıcı logout yapıp yeniden login oldu. Bu sayede:

  • Eski session silindi
  • Yeni session oluştu
  • Fresh user object + fresh roles yüklendi

🔄 Neden Sorun Tekrar Ediyor?

Yukarıdaki çözüm geçici bir çözümdür. Aşağıdaki durumlardan herhangi biri gerçekleştiğinde sorun tekrar edecektir:

1. Config Cache Yenilendiğinde
php artisan config:cache komutu çalıştırıldığında, Spatie Permission cache'i yeniden oluşturulur ve yine tenant prefix olmadan yazılır.
2. Role/Permission Değişikliğinde
Admin panelden rol veya yetki değişikliği yapıldığında, Spatie otomatik cache invalidation yapar. Yeni cache yazılırken yine tenant context olmayabilir → prefix mismatch tekrar oluşur.
3. Cache TTL Dolduğunda
Spatie Permission cache TTL'i 24 saat. Cache expire olduğunda yenilenirken yine aynı sorun oluşabilir.
4. Deployment Sonrası
Production deploy edildikten sonra genellikle php artisan optimize veya config:cache çalıştırılır. Yine aynı problem.

⚠️ Sonuç: Bu geçici çözüm, acil durumda işe yarar ama kalıcı değildir. Sistem yapılandırması değiştirilmediği sürece sorun periyodik olarak tekrar edecektir.

🛠️ Kalıcı Çözüm Önerileri

ÖNERİLEN

Çözüm 1: Permission Cache'i Array Driver Yap

📋 Açıklama:

Spatie Permission cache'ini Redis yerine array driver kullanacak şekilde değiştir. Bu sayede cache sadece request süresince bellekte kalır, Redis'e yazılmaz. Tenant prefix sorunu ortadan kalkar.

💻 Uygulama:
// config/permission.php
'cache' => [
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
'key' => 'spatie.permission.cache',
'store' => 'array', // 'default' yerine 'array'
],
✅ Avantajlar
  • Tenant prefix sorunu yok
  • Her request fresh data
  • Kolay uygulama (1 satır değişiklik)
  • Side effect yok
❌ Dezavantajlar
  • Her request'te DB query (performans)
  • Yüksek trafikte yük artışı
  • Redis cache avantajı kaybedilir
💡 Öneri: Müzik sitesi için ideal. Roller nadiren değişir, kullanıcı sayısı sınırlı. Permission check overhead'i kabul edilebilir seviyede kalır.
ALTERNATİF

Çözüm 2: Permission Cache'i Devre Dışı Bırak

📋 Açıklama:

Spatie Permission cache'ini tamamen kapatır. Her permission check'te direkt database'den okur.

💻 Uygulama:
// config/permission.php
'cache' => [
'expiration_time' => 0, // Cache TTL sıfır
'key' => 'spatie.permission.cache',
'store' => 'array',
],
⚠️ Uyarı: Çözüm 1 ile neredeyse aynı. Fark: Cache mekanizması tamamen bypass edilir. Performans etkisi biraz daha fazla olabilir.
ADVANCED

Çözüm 3: Tenant-Aware Custom Cache Key

📋 Açıklama:

Spatie Permission cache key'ini tenant ID ile dinamik oluştur. Her tenant kendi cache key'ini kullanır. Bu çözüm Redis cache avantajını korur ama implement etmesi daha karmaşık.

💻 Uygulama (Konsept):
// app/Providers/AppServiceProvider.php
public function boot()
{
PermissionRegistrar::setCacheKey(function() {
$tenantId = tenant() ? tenant()->id : 'central';
return "tenant{$tenantId}_spatie.permission.cache";
});
}
✅ Avantajlar
  • Redis cache avantajı korunur
  • Tenant isolation tam
  • Performans optimal
❌ Dezavantajlar
  • Spatie API değişikliği gerekebilir
  • Test ve debug zor
  • Package update'te sorun çıkabilir
  • Kompleks implementation
⚠️ Not: Bu çözüm Spatie Permission'ın internal mekanizmasını override etmek gerektirebilir. Muhtemelen PermissionRegistrar sınıfını extend etmek ve getCacheKey() metodunu override etmek gerekir. Advanced seviye PHP bilgisi gerektirir.

🎯 Hangi Çözümü Seçmeli?

Muzibu İçin Öneri:

Çözüm 1 (Array Driver) en uygun seçenek.

  • Müzik sitesi: Yüksek trafik yok
  • Kullanıcı sayısı: ~100 aktif, admin ~2-3 kişi
  • Permission check: Sadece admin panelde (kullanıcı tarafında yok)
  • Uygulama: 1 satır değişiklik, hemen production'a alınabilir
  • Risk: Çok düşük, side effect yok
Eğer Yüksek Trafik Varsa:

Çözüm 3 (Custom Cache Key) daha uygun olur. Ama Muzibu için şu aşamada gerekli değil.

💡 Neden Geçici Çözüm İşe Yaradı?

❌ Önce (Bozuk)

Spatie cache bozuk (prefix mismatch)
$user->roles = [] (boş)
Session'da eski user object
hasRole('root') = false
Middleware 403 döndü

✅ Sonra (Geçici Düzeldi)

syncRoles() → cache refresh
$user->roles = [root] ✓
Redis flush → session silindi
Login → fresh user + roles
Middleware erişim verdi

核心 (Core): syncRoles() fonksiyonu sadece database'e yazmadı, aynı zamanda Spatie Permission'ın internal cache'ini de temizledi. Ardından Redis flush ve logout/login ile session'daki eski user object'i yenilendi. Bu üçlü kombinasyon sorunu geçici olarak çözdü. Ama kök sebep (tenant prefix mismatch) hala var, bu yüzden tekrar edebilir.

🛡️ Gelecek İçin Önlemler

1.

Kalıcı Çözümü Uygula: Yukarıdaki "Çözüm 1: Array Driver" önerisini uygula. Bu sorunu kökten çözer.

2.

Role değişikliklerinde cache temizle: Role atama/çıkarma işlemlerinden sonra php artisan permission:cache-reset çalıştır (eğer Redis cache kullanmaya devam edersen).

3.

Admin role değişikliği izni: Kritik roller (root, admin) değiştirildiğinde kullanıcıyı otomatik logout et.

4.

Monitoring: AdminAccessMiddleware'de role check fail olduğunda log at (debug için).

5.

Deployment checklist: Production deploy sonrası config:cache çalıştırıyorsan, ardından permission:cache-reset de çalıştır (eğer Redis cache kullanıyorsan).

📁 İlgili Dosyalar

Middleware
app/Http/Middleware/AdminAccessMiddleware.php
Horizon Gate
app/Providers/HorizonServiceProvider.php
User Model
app/Models/User.php
Permission Config
config/permission.php
⚠️ Kalıcı çözüm için değiştirilmeli
Tenancy Config
config/tenancy.php
Redis prefix yapılandırması
Cache Config
config/cache.php
Default store tanımı
Horizon Config
config/horizon.php
Trait
Modules/UserManagement/app/Traits/HasModulePermissions.php